Что такое бизнес логика в программировании
Что такое бизнес-логика
Логика, да не совсем та
Дорогие читатели, в 100% случаев вы используете логику для того, чтобы разобраться в том, как работает продукт, который вы делаете и вам кажется, что именно это и есть UX. Основную часть того самого UX составляет бизнес-логика. Скорее всего вы спросите, почему дизайнера вообще должен волновать вопрос бизнеса. Ну логика-то ладно, а что такое бизнес-логика?
Давайте разберемся, что же такое бизнес-логика:
Бизнес-логика описывает работу всех бизнес-процессов, существующих в продукте.
Воу-воу-воу, полегче.. Это же и есть UX!
И да и нет. Под термином «Бизнес-логика» действительно понимают UX, но есть существенный нюанс.
Обычно к UX-дизайну относятся только пользовательские сценарии. Тогда как бизнес-логика описывает именно бизнес-процессы, происходящие под капотом UX с сугубо технической точки зрения.
Если бизнес-логика отвечает на вопрос:
«Как продукт должен работать технически?»
То UX-дизайн отвечает на вопрос:
«Как пользователь будет пользоваться продуктом и как сделать этот процесс максимально удобным и быстрым?».
Наглядная разница
UX-дизайн рассматривает ситуации (сценарии), с которыми сталкивается пользователь в процессе использования продукта; проблемы, которые продукт должен решить, чтобы им было интересно, выгодно или, как минимум, удобно пользоваться.
Бизнес-логика, наоборот, есть набор различных бизнес-процессов, возникающих внутри продукта. Они связаны между собой сугубо технически и никак не связаны с UX-дизайном.
UX-дизайн
То, как видит логику работы части приложения UX-дизайнер.
Бизнес-логика
То, что должен видеть хороший UX-дизайнер и продакт менеджер.
В реальной жизни бизнес-процесс устроен несколько сложнее, но общая идея и различие с UX-дизайном должны быть понятно.
Тэйк эвэй
Термин «Бизнес-логика» вы вряд ли услышите в стартапе, нацеленном на продажу смуззи, зато этот термин в широком ходу в B2B и интеграторах.
Теперь вас точно не испугаешь замысловатым вопросом «А как устроена бизнес-логика?». Помните, что бизнес-логика – это про весь механизм работы продукта, а UX-дизайном является лишь то, что по факту увидит пользователь в интерфейсе, в email и sms, отправленные вашим продуктом.
Бизнес-логика в no-code: что это и как ее построить
Бизнес-логика приложения – это, по сути, описание схем, по которым приложение взаимодействует с пользователем. Когда пользователь оформляет подписку, или заполняет форму заказа, или просто авторизуется – все эти действия обрабатываются «под капотом» приложения в определенном порядке.
Какие данные нужно запросить? Соответствуют ли введенные данные заданному формату? Что произойдет после того, как пользователь нажмет кнопку «Подтвердить»? А есть ли вообще у него права доступа к данной операции? На все эти и многие другие вопросы можно ответить, изучив, как построена бизнес-логика конкретного приложения.
Простейший пример: администратор авиакомпании (пользователь) регистрирует пассажира на рейс (вносит информацию в базу данных).
1.Открывает информацию о выбранном рейсе, переходит к списку уже зарегистрированных пассажиров, нажимает «Зарегистрировать пассажира».
2.Заполняет форму регистрации: вводит номер рейса, выбирает пассажира, указывает место и статус регистрации.
3.Нажимает кнопку «Подтвердить»
4.Видит нового пассажира в общем списке.
1.Приложение проверяет, авторизован ли пользователь и имеет ли права доступа к выбранной странице, а также операции регистрации.
2.Ждет, пока пользователь заполнит форму.
3.Обрабатывает введенные данные:
a. Проверяет, соответствуют ли введенные данные требованиям приложения (эти требования заранее прописаны программистом): например, в поле «Номер рейса» должно быть целое число.
b. Получает из базы данных информацию: например, о рейсе и связанных с ним регистрациях (чтобы внести изменения), пассажире (чтобы проверить, действительно ли этот пассажир есть в базе данных).
c. Выдает сообщения об ошибках, если поля заполнены неверно.
d. Отправляет информацию в базу данных, отдавая команды на создание в ней новых записей или обновлении существующих.
4.Выводит обновленную информацию на экран.
Общая логика приложения строится из бизнес-процессов – схем, описывающих конкретные операции в системе: создание записи о пассажире, добавление в систему нового рейса, редактирование информации о регистрации.
Когда речь идет о классическом программировании, то для описания всех процессов используются блоки кода. Многие из них пишутся по шаблонам – просто используются в разной последовательности и для работы с разными данными.
Именно благодаря этой «шаблонности» в no-code разработке появилась возможность использовать инструменты визуального программирования – так называемые дизайнеры бизнес-логики. Они помогают выбрать нужные блоки, скомпоновать в нужной последовательности, настроить. И даже создать некоторые блоки автоматически, в зависимости от настроек других компонентов приложения. Итог – готовая бизнес-логика без необходимости проводить многие часы над строками кода.
Celesta и Flute: Создание бизнес-логики в Java-экосистеме
Привет, Хабр! Проект, о котором мы расскажем, с самого начала создавался нами как open-source, но до недавних пор мы использовали его только лишь для своих нужд, не говорили о нём широко и не создавали коммьюнити. Cейчас, спустя несколько лет разработки, мы почувствовали уверенность в том, что настала пора рассказать про него, и надеемся, что он начнёт приносить пользу не только нам.
Celesta (челеста) — «движок» бизнес-логики на языке Jython, встраиваемый в Java-приложения. Flute (флейта) — компонент, который позволяет работать челесте как сервису. Сначала мы расскажем о том, зачем такое вообще бывает нужно, но если вы хотите пропустить вступление и перейти сразу к технической части, то вам — в Часть II.
Часть I, вводная
Для чего нужна Celesta?
Решаемая проблема такова: как встроить бизнес-логику в Java-приложение или, шире, в приложение, работающее в Java-экосистеме.
Казалось бы, зачем тут изобретать очередной «велосипед»? Ведь мы знаем, что есть специальные системы для работы с бизнес-логикой и написания бизнес-приложений. Самая распространённая в России — «1C», есть Microsoft Dynamics, SAP и многие другие. Причём в работе с подобными программными продуктами задействовано, судя по всему, не меньше половины всех ИТ-специалистов по всему миру. По крайней мере, основатель «1C» утверждает, что из миллиона ИТ-специалистов в России треть — специалисты «1С».
В то же время, всегда есть более локальные задачи, где привлечение подобных систем проблематично. Допустим, имеется интернет-магазин, а «позади» него должна стоять некоторая бизнес-логика, обрабатывающая заказы. Можно ли поставить для обработки бизнес-логики одну из вышеперечисленных больших систем? Вполне. Но неудобство заключается в том, что если весь магазин, допустим, написан в Java-экосистеме, то система создания бизнес-приложений — уже совсем не в Java-экосистеме, что затрудняет интеграцию. Система дорога в лицензировании и поддержке, требует особых специалистов. А задачи, которые в этих случаях возлагаются на данную систему, не такие уж глобальные. Цена вопроса оказывается неоправданно высокой, и решение калибра Celesta может выглядеть неплохой альтернативой.
Очень часто возникают задачи, связанные с реализацией какого-либо процесса работы с документом. Например, согласование договора или заявки на оплату. Классическим решением является использование систем типа Documentum, Alfresco и т.п. (часто используется термин CMS + BPM, т.е. Управление контентом + Управление бизнес-процессами). Однако, это все довольно сложные инфраструктурные системы. Имеет смысл их использовать, если необходимо обеспечивать работу с большим количеством документов разного типа и поддерживать много бизнес-процессов. А если не хочется выходить за рамки конкретного проекта? Celesta + Activiti прекрасно решат задачу. Celesta при этом обеспечит содержательную работу с документом, а Activiti будет показывать, что и в каком порядке должно выполняться.
Поэтому мы решили создать решение, которое позволило бы нам, не выходя за пределы Java-экосистемы, и не вводя новые большие составляющие в инфраструктуру, создавать вполне эффективные модули, управляющие бизнес-логикой, необходимые нашему заказчику.
За несколько лет работы мы внедрили решения на базе нашей платформы в достаточно многих организациях, некоторые из которых перечислены на сайте платформы.
Чем «бизнес-логика» отличается от просто логики?
Почему вообще нужны особенные системы для бизнес-логики? Почему «нельзя просто взять» и написать на Java, скажем, учёт финансов или товарных остатков? Ведь, казалось бы, какая разница, где складывать денежные суммы — в Java или в 1C (в Java, причём, вычисления-то наверняка побыстрее будут). Почему же у нас есть 1C, SAP и им подобные платформы?
Проблема в первую очередь заключается в изменчивости логики системы. Бизнес-приложение невозможно создать «раз и навсегда»: изменения в требования к бизнес-приложению поступают непрерывным потоком на всех этапах его жизненного цикла: разработки, внедрения, эксплуатации — потому что живёт и развивается бизнес-процесс, который приложение должно автоматизировать. Можно подумать, что такое количество изменений — результат неправильного первоначального анализа или плохой организации всего процесса. Но нет, это объективное свойство реальной жизни, причём не только в коммерческих организациях. Например, в государственных структурах требования по сути определяются нормативными документами (законами, постановлениями, приказами и т.п.). Бывает так, что проект для государственной структуры надо сдавать к некоторой дате, но до последнего момента неизвестно, подпишет или не подпишет премьер-министр постановление, от которого будут зависеть функциональные требования к системе, неизвестно бывает и его точное содержание.
Другая специфическая черта бизнес-приложений — потребность в обеспечении целостности данных. Если у нас учтена продажа, она должна отразиться во всех необходимых книгах операций. Если, скажем, продажа, отражаясь в подсистеме, ответственной за логистику, не отражается в бухгалтерском балансе — это приведет к большим проблемам.
Третья специфическая особенность — невозможность спрогнозировать требования доступа к данным. На начальном этапе даже бывает невозможно чётко определить, в каком формате данные потребуются на выходе, как они будут сегментированы, в каких документах. Необходимо закладывать такую систему, чтобы вывод данных можно было изменять достаточно быстро, на ходу.
Как обычно решения для бизнес-логики справляются с этими задачами?
Во-первых, за счёт разделения кода на платформу и бизнес-логику. Это применяется во всех системах такого класса. Есть код платформы, обеспечивающий базовые вещи, и есть код бизнес-логики, который пишется чаще всего на специализированном языке: например, 1C, Microsoft Dynamics и SAP предлагают собственные языки для написания бизнес-логики.
Код платформы, решающий самые базовые задачи, производят авторы платформы и изменяют его только вместе с выходами новых версий платформы. Код бизнес-логики изменяется разработчиками бизнес-логики постоянно.
Во-вторых (в наше время это особенно приходится подчеркнуть) – использование реляционных СУБД. Несмотря на сильное развитие NoSQL-баз, для решения задач построения бизнес-логики на сегодня лучшим инструментом остаются реляционные СУБД. Лучшим — хотя бы потому, что это более старые, более зрелые проекты. Все основные преимущества реляционных СУБД, не присутствующие во многих из NoSQL-баз, остаются востребованными. Это и атомарность операций — возможность в случае ошибки откатить транзакцию с большим количеством изменений, так, как будто она и не начиналась. Это и изоляция. Это и обеспечение целостности через внешние ключи. И возможность обеспечить быстрое извлечение данных в произвольном формате. Не забудем также о необходимости интеграции с огромным количеством legacy-систем, данные которых находятся в реляционных СУБД. В общем, реляционные СУБД были и остаются главным инструментом хранения данных в подобных системах.
Чем создание бизнес-логики не отличается от «обычного» кодирования?
С вещами, делающими разработку бизнес-логики «особенной», понятно. Чем же она похожа на весь остальной программный код — будь то игры или операционные системы?
Где здесь место для Celesta?
На практике совместить платформу разработки бизнес-логики и перечисленные требования практически нельзя. Мы имеем две крайности.
Одна крайность – тотальное использование крупной системы типа 1C, Microsoft Dynamics, SAP и т. п. для решения вообще любых задач. Зачастую эти системы сковывают разработчиков, лишают их привычных инструментов и методов разработки (например, нельзя стандартными инструментами произвести автоматизированное тестирование), это повышает стоимость и удлиняет сроки разработки. Для типовых задач большого масштаба это оправдано, однако для небольших задач этот подход может оказаться губительным.
Противоположная крайность заключается в том, чтобы браться за любую задачу на системе разработки общего назначения. Открываем IDEA, создаём новый Java-проект, а там посмотрим — удастся или нет нам реализовать, например, для системы онлайн-продаж финансовый учёт и оборотно-сальдовую ведомость. Что в этом сложного? На первый взгляд ничего, пока не начнёте делать и не убедитесь в том, что без трудоёмкой реализации специальных паттернов и подходов сделать это нельзя, и что это отнимает все ваши ресурсы. Мы не утверждаем, что так добиться успеха невозможно, но есть определённые вещи, за которые браться не стоит.
Celesta здесь занимает промежуточное положение. Будучи Java-библиотекой (celesta.jar), она является «движком» бизнес-логики. Это «движок», который либо встраивается в Java-приложение, либо с помощью модуля Flute существует самостоятельно и обеспечивает возможность быстрой и правильной реализации бизнес-логики.
Сама Celesta написана на Java, а бизнес-логика пишется на языке Jython. Jython – это Java-реализация Python. Сейчас она имеется для версии Python 2.7. Изящность Python-кода, лёгкость освоения играли не последнюю роль при выборе языка для бизнес-логики, и он с нами уже несколько лет.
Однако мы не привязываемся к Python/Jython так уж сильно. Нам годится любой скриптовый язык, в последнее время мы присматриваемся к тому, чтобы встроить в Celesta Groovy.
Часть II, техническая
Что такое Celesta и что она умеет?
Место платформы Celesta как промежуточного слоя между реляционной базой и кодом бизнес-логики на общей картинке можно изобразить так:
Мы поддерживаем четыре типа реляционных БД и код бизнес-логики на Jython. При этом Celesta немного присутствует и внутри базы данных, создавая для себя служебные объекты и триггеры.
Основные функциональные возможности Celesta:
Для чего нужна независимость от типа СУБД?
Независимость кода бизнес-логики от типа СУБД мы поставили первым пунктом не случайно: код, написанный для Celesta, вообще не знает, на какой СУБД он исполняется. Зачем это сделано?
Во-первых, из-за того, что выбор типа СУБД – это вопрос не технологический, а политический. Приходя к новому заказчику, мы чаще всего обнаруживаем, что у него уже есть Oracle или SQL Server, в который инвестированы средства, и заказчик хочет видеть и другие решения на существующей инфраструктуре. Технологический ландшафт постепенно меняется: в госструктурах и частных компаниях все больше встречается PostgreSQL, хотя ещё несколько лет назад в нашей практике превалировал MS SQL Server. Celesta поддерживает наиболее часто встречающиеся СУБД, и нас эти изменения не тревожат.
Во-вторых, код, уже созданный для решения стандартных задач, хотелось бы переносить от одного заказчика другому, создавать переиспользуемую библиотеку. Вещи вроде иерархических справочников или модулей рассылки уведомлений на email по сути своей стандартны, и зачем нам поддерживать несколько версий под заказчиков с разными реляционками?
В-третьих — последнее по порядку, но не важности — возможность запуска модульных тестов без использования DbUnit и контейнеров с использованием базы данных H2, работающей в режиме in-memory. В этом режиме база H2 запускается моментально. Celesta очень быстро создаёт в ней схему данных, после чего можно провести необходимые тесты и «забыть» базу. Так как код бизнес-логики действительно не знает, на какой базе он бежит, то соответственно, если он без ошибок отрабатывает на H2, то без ошибок он будет работать и на PostgreSQL. Конечно, в задачу разработчиков самой системы Celesta входит сделать все тесты с задействованием подъема реальных СУБД, чтобы убедиться, что наша платформа одинаково свой API выполняет на разных реляционках (и мы это делаем). Но разработчику бизнес-логики этого уже не требуется.
CelestaSQL
За счет чего достигается «кроссбазданческость»? Конечно, за счёт того, что с данными можно работать только через специальный API, изолирующий логику от любой специфики БД. Celesta кодогенирирует Python-классы для доступа к данным, с одной стороны, и SQL-код и некоторые вспомогательные объекты вокруг таблиц, с другой стороны.
Celesta не предоставляет object-relational mapping в чистом виде, потому что при проектировании модели данных мы исходим не от классов, а от структуры базы данных. Т. е. сначала выстраиваем ER-модель таблиц, а затем на основе этой модели Celesta сама генерирует классы-курсоры для доступа к данным.
Достигнуть одинаковой работы на всех поддерживаемых СУБД можно только лишь для той функциональности, которая приблизительно одинаково реализована в каждой из них. Если условно в виде «кругов Эйлера» изобразить множества функциональных возможностей каждой из поддерживаемых нами баз, то получается такая картина:
Если мы обеспечиваем полную независимость от типа БД, то те функциональные возможности, которые мы открываем программистам бизнес-логики, должны лежать внутри пересечения по всем базам. На первый взгляд кажется, что это существенное ограничение. Да: какие-то специфические возможности, допустим, SQL Server мы не можем использовать. Но все без исключения базы поддерживают таблицы, внешние ключи, представления, SQL-запросы с JOIN и GROUP BY. Соответственно, мы можем дать эти возможности разработчикам. Мы предоставляем разработчикам «обезличенный SQL», который называем «CelestaSQL», а в процессе работы мы модифицируем SQL-запросы для диалектов соответствующих баз.
У каждой базы данных есть свой набор типов данных. Т. к. мы работаем через язык CelestaSQL, у нас тоже есть свой набор типов. Их всего семь, вот они и их сопоставление с реальными типами в базах:
CelestaSQL | Microsoft SQL Server | Oracle | PostgreSQL | H2 | |
Integer (32-bit) | INT | INT | NUMBER | INT4 | INTEGER |
Floating point (64-bit) | REAL | FLOAT(53) | REAL | FLOAT8 | DOUBLE |
String (Unicode) | VARCHAR(n) | NVARCHAR(n) | NVARCHAR2(n) | VARCHAR(n) | VARCHAR(n) |
Long string (Unicode) | TEXT | NVARCHAR (MAX) | NCLOB | TEXT | CLOB |
Binary | BLOB | VARBINARY (MAX) | BLOB | BYTEA | VARBINARY (MAX) |
Date/Time | DATETIME | DATETIME | TIMESTAMP | TIMESTAMP | TIMESTAMP |
Boolean | BIT | BIT | NUMBER CHECK IN (0, 1) | BOOL | BOOLEAN |
Может показаться, что всего семь типов — это мало, но на самом деле это те самые типы, которых всегда достаточно, чтобы хранить финансовую, торговую, логистическую информацию: строк, целых чисел, дробных, дат, boolean-значений и BLOB-ов всегда хватит для представления таких данных.
Сам язык CelestaSQL описан в документации с большим количеством диаграмм Вирта.
Модификация структуры базы данных. Идемпотентный DDL
Еще одна ключевая функциональная возможность Celesta – это подход к модификации структуры, которая должна происходить на «живой» базе данных.
Какие вообще имеются возможные подходы к решению задачи контроля изменений структуры базы данных?
Есть очень распространенный подход, который можно условно назвать «лог изменений». Liquibase — наиболее известный в Java-мире инструмент, который решает задачу таким образом. В Python-мире тем же самым занимается фреймворк Django. Этот подход заключается в постепенном наращивании лога изменений базы данных, database change log. По мере того, как в структуре базы надо производить изменения, вы добавляете к этому логу инкрементные change set-ы. Постепенно ваш лог изменений накапливается, вбирая в себя всю историю модификаций вашей БД: ошибочных, исправляющих, рефакторингов и т. п. Через какое-то время изменений становится настолько много, что понять текущую структуру таблиц непосредственно по логу становится невозможно.
Хотя на сайте системы Liquibase и пишут, что их подход обеспечивает рефакторинг и контроль версий структуры базы данных — ни то, ни другое по-настоящему при помощи database change log-а не достигается. Понять это довольно просто, сравнив с тем, как вы выполняете рефакторинг обычного кода. Если, например, вам необходимо добавить какие-то методы в класс, то вы их добавляете непосредственно в определение класса, а не дописываете в change log код вроде «alter class Foo add method bar <. >». То же и с контролем версий: при работе с обычным кодом сама система контроля версий создаёт для вас лог изменений, а не вы дописываете changeset-ы в конец какого-нибудь журнала.
Понятно, что для структуры базы данных так делается неспроста: причина в том, что в таблицах базы уже существуют данные, и change set призван конвертировать не только структуру, но и ваши данные. Таким образом change log как будто дает уверенность в том, что вы всегда сможете обновиться с его использованием с любой версии базы данных. Но на самом деле это ложная уверенность. Ведь если вы протестировали код модификации ваших данных на какой-то копии базы данных и он сработал, нет гарантии, что он же сработает на базе с какими-то другими данными, где могут быть какие-то особые случаи, которых вы не учли в вашем changeset-е. Самое неприятное, что может случиться с такой системой — это changeset, отработавший наполовину и закоммитивший часть изменений: база оказывается «посередине» между версиями, и потребуется ручное вмешательство, чтобы исправить ситуацию.
Есть другой подход, условно назовем его «configuration management-подход» или иначе — «идемпотентный DDL».
По аналогии с тем, как configuration management системах типа Ansible у вас есть идемпотентные скрипты, которые говорят не «сделай что-то», а «приведи что-то к желаемому состоянию», точно так же и мы, когда пишем на CelestaSQL следующий текст:
— этот текст интерпретируется Celesta не как «создай таблицу, а если таблица уже есть, то выдай ошибку», а «приведи таблицу к желаемой структуре». То есть: «если таблицы нет — создай, если таблица есть, посмотри, какие в ней поля, с какими типами, какие индексы, какие внешние ключи, какие default-значения и т. п. и не надо ли что-то изменить в этой таблице, чтобы привести её к нужному виду».
При таком подходе мы достигаем настоящего рефакторинга и настоящего контроля версий на наших скриптах определения структуры базы:
На практике, создание «вспомогательных» скриптов требуется нечасто. Абсолютное большинство изменений (добавление полей, перестройка индексов, изменение views) производятся в Celesta автоматически «на ходу».
Структура проекта Celesta. Гранулы
Для того, чтобы начать пользоваться Celesta, нужно понять, как устроен Celesta-проект с бизнес-логикой.
Совокупность всей бизнес-логики мы называем «score» («партитура»), внутри «score» находятся «grains» — гранулы, они же модули:
Здесь пунктирными стрелками показаны зависимости, то есть гранулы могут использовать объекты из других гранул. И эти зависимости могут быть сложными, но главное ограничение состоит в том, чтобы зависимости по внешним ключам не были циклическими — это нужно для обеспечения успешного обновления структуры БД по гранулам, когда Celesta начинает с того, что выбирает правильный порядок обновления.
С точки зрения исходных кодов, гранула – это папка. Требования к папке следующие:
С точки зрения Python (или Jython в нашем случае) гранула — это пакет, в котором будут находиться сгенерированные классы доступа к данным, и в котором можно будет создать свои модули с кодом бизнес-логики. Поэтому также в папке гранулы должен находиться файл с именем __init__.py
Запуск Celesta и синхронизация структуры базы
При запуске Celesta занимается синхронизацией структуры базы данных. Примерная последовательность шагов такова:
Это весьма краткий пересказ того, что происходит на многоэтапном процессе запуска, включающем в себя и генерацию классов для доступа к данным, и инициализацию пакетов гранул. Более детально процесс запуска описан на этой странице документации.
Создание модели данных и базы данных в Celesta
Давайте посмотрим, как в Celesta можно создавать модель данных и разворачивать базу данных.
Допустим, мы делаем проект для компании интернет-торговли, которая недавно объединилась с другой компанией. У каждой есть своя база данных. Они собирают заказы, но пока они не слили свои базы данных воедино, нужна единая точка входа для того, чтобы собирать заказы, поступающие извне.
Для начала нам надо создать структуру таблиц, хранящих заказы. Заказ, как известно, сущность составная: он состоит из заголовка, где хранится информация о клиенте, дате заказа и прочих атрибутов заказа, а также из множества строк (товарных позиций).
Итак, за дело: создаём
Первый этап сделан: модель данных построена в первом приближении, и теперь нам хотелось бы применить её к базе данных. Для этого мы создаём пустую базу данных и напишем простое Java-приложение, использующее Celesta.
Используем Maven-dependency для Celesta (актуальную версию можно взять на сайте corchestra.ru):
Создаём boilerplate-код и запускаем его:
Через объект Properties передаются базовые настройки Celesta, такие как путь к папке score (её подпапкой должна быть /orders), путь к стандартной библиотеке Jython (Jython должен быть установлен на вашей машине!) и параметры JDBC-подключения к базе данных. Полный перечень параметров Celesta приведён в wiki-документации.
Если параметры заданы правильно и всё прошло успешно, то можно посмотреть, что случилось с базой данных mytest. Мы увидим, что в базе появилась схема orders с нашими таблицами «OrderHeader» и «OrderLine», а также представление «OrderedQty». Теперь допустим, что спустя какое-то время мы решили изменить нашу модель данных. Допустим, мы хотим в заголовке заказа расширить поле с именем клиента до 100 символов и добавить поле с кодом менеджера. Сделать это мы можем прямым редактированием определения таблицы в файле _orders.sql, буквально изменив одну строку и и дописав другую:
Запустив приложение ещё раз, мы можем убедиться, что структура базы данных изменилась, чтобы отвечать новой модели.
Помимо схемы orders, в базе данных создаётся служебная схема celesta. Полезно заглянуть в таблицу grains, чтобы увидеть в ней запись о грануле orders, её статусе и контрольной сумме скрипта _orders.sql.
Создание Celesta-процедур: контексты сессии и вызова, запуск «hello, world!»
Разобравшись с созданием структуры базы данных, можно приступать к написанию бизнес-логики.
Для того, чтобы можно было реализовать требования распределения прав доступа и логирования действий, любая операция над данными в Celesta производится от имени некоторого пользователя, «анонимных» операций быть не может. Поэтому любой Celesta-код выполняется в некотором контексте вызова, который, с свою очередь, существует в контексте сессии.
Появление и удаление контекста сессии через методы login/logout позволяют осуществлять аудит входов-выходов. Привязка пользователя к контексту определяет разрешения на доступ к таблицам, а также обеспечивает возможность логирования изменений, производимых от его имени.
Чтобы убедиться, что мы можем запускать код Celesta-процедур как таковой, для начала рассмотрим пример «Hello, world», а потом построим менее тривиальную систему, которая будет модифицировать данные в базе и использовать модульные тесты для проверки своей корректности.
Вернёмся в папку score/orders и создадим в ней Python-модуль hello.py следующего содержания:
Любая Celesta-процедура должна своим первым аргументом иметь context, который является экземпляром класса ru.curs.celesta.CallContext — в нашем примитивном примере он не требуется, но как мы увидим далее, он играет ключевую роль. Кроме того, Celesta-процедуры могут иметь произвольное количество других дополнительных параметров (в том числе не иметь вовсе). В нашем примере присутствует один дополнительный параметр name.
Чтобы запустить Celesta-процедуру, её нужно идентифицировать по трехкомпонентному имени. Внутри гранулы orders у нас находится питоновский модуль hello, внутри которого находится функция run — значит, трёхкомпонентное имя нашей процедуры будет orders.hello.run. Если бы мы использовали несколько вложенных питоновских модулей, тогда их имена также можно было бы перечислить через точку, например: orders.subpackage.hello.run.
Модифицируем немного наш код на Java, дописав создание контекстов сессии и вызова и, собственно, запуск процедуры:
Запустив Java-программу, мы получим приветствие от питоновского кода, который выполняется из-под Celesta.
Создание Celesta-процедур: модификация данных, защита от race conditions и транзакции
Теперь мы покажем, как написать на Celesta код, читающий и изменяющий данные в базе. Для этого мы используем так называемые курсоры — классы, которые Celesta сгенерировала для нас. Мы можем увидеть что они из себя представляют, зайдя в папку с гранулой orders: т. к. мы уже запускали Celesta, то кодогенерация была выполнена, и в папке orders будет находиться файл _orders_orm.py.
Внутри него обнаружатся классы курсоров OrderHeaderCursor, Order LineCursor и OrderedQtyCursor. Как видим, по одному классу создано на каждый из объектов гранулы – на две таблицы и одно представление. И теперь эти классы мы можем использовать для доступа к объектам базы данных в нашей бизнес-логике.
Чтобы создать курсор на таблицу заказов и выбрать первую запись, нужно написать такой Python-код:
После создания объекта header мы можем получить доступ к полям записи таблицы через переменные:
Как мы уже говорили, первым аргументом любой Celesta-процедуры является контекст вызова, и этот контекст мы обязаны передать в качестве первого аргумента конструктора любого курсора — это единственный способ создать курсор. Контекст вызова несёт в себе информацию о текущем пользователе и его правах доступа.
С объектом-курсором мы можем производить разные вещи: фильтровать, переходить по записям, а также, естественно, вставлять, удалять и обновлять записи. Весь API курсоров подробно описан в документации.
Например, код нашего примера можно было бы развить следующим образом:
В этом примере мы выставляем фильтр по полю city, затем находим первую запись методом tryFirst.
В момент срабатывания tryFirst переменные курсора заполняются данными одной записи, мы можем читать и присваивать им значения. А когда данные в курсоре полностью подготовлены, мы выполняем update(), и он сохраняет содержимое курсора в базе данных.
Какой проблеме может быть подвержен этот код? Конечно же, возникновению race condition/lost update! Потому что между моментом, когда мы получили данные в строке с «tryFirst», и моментом, когда мы пытаемся обновить эти данные в точке «update», кто-то другой уже может получить, изменить и обновить эти данные. После того, как данные прочитаны, курсор никаким образом не блокирует их использование другими пользователями! Потерянные обновления были бы большой проблемой в такой системе, но Celesta содержит защиту, основанную на проверке версий данных. В каждой таблице по умолчанию Celesta создаёт поле recversion, и на уровне ON UPDATE-триггера выполняет инкремент номера версии и проверяет, что обновляемые данные имеют ту же версию, что и в таблице. Если произошла проблема — выбрасывает исключение. Подробнее об этом можно прочитать в статье документации «защита от потерянных обновлений».
В случае, если выход из Celesta-процедуры происходит по необработанному исключению, Celesta откатывает неявную транзакцию, которую она начинает перед выполнением процедуры. Важно понимать, что call context — это не только контекст вызова, но ещё и транзакция. Если Celesta-процедура заканчивается успешно, тогда происходит commit. Если Celesta-процедура заканчивается с необработанным исключением, тогда происходит rollback.
Специалист, который пишет бизнес-логику, может не знать всех тонкостей, происходящих «за кулисами»: он просто пишет бизнес-логику, а система обеспечивает консистентность данных. Если ошибка происходит в какой-то сложной процедуре — откатывается вся связанная с контекстом вызова транзакция, как будто бы мы ничего и не начинали делать с данными, данные не испорчены. Если же зачем-то нужен commit в середине, допустим, какой-то большой процедуры, то явный commit можно выполнить, вызвав context.commit().
Создание Celesta-процедур: модульное тестирование
Давайте рассмотрим более продвинутый пример.
Допустим, у нас имеются вот JSON-файлы, которые мы хотим класть в базу данных, состоящую из
В каждом из этих JSON у нас есть поля, относящиеся к заголовку заказа, и есть массив, относящийся к его строкам. Как быстро и надёжно создать приложение, которое обрабатывает эти данные и укладывает в СУБД? Конечно, через тестирование!
Начнём с того, что создадим класс модульного теста, который наследуем от CelestaUnit. В свою очередь, CelestaUnit является наследником unittest.TestCase системы PyUnit:
И напишем модульный тест для проверяемой процедуры:
Обратите внимание на то, что мы имеем возможность писать модульные тесты в предположении, что к моменту их выполнения база данных будет абсолютно пустой, но со структурой, которая нам нужна, а после их выполнения мы можем не заботиться о том, что мы оставили «мусор» в базе. Более того: используя импорт CelestaUnit, мы можем вовсе не заботиться о том, чтобы хоть какая-то БД стояла у нас на рабочей машине. CelestaUnit поднимает H2 in-memory базу и все конфигурирует за нас, а нам остаётся только брать из self готовый контекст вызова и пользоваться им для создания курсоров.
Если запустить этот тест сразу, то он не сработает, т. к. мы не реализовали тестируемый метод. Напишем его:
Снова запустим тест в IDE и ура:
Мы также можем добавить в тест какие-то более сложные проверки, например, что строки заказа вставились, что их ровно две и т. д.
Давайте создадим вторую процедуру, возвращающую JSON с агрегированными значениями, показывающими, сколько каких товаров заказали у нас.
Тест записывает в базу два заказа, после чего проверяет суммарное значение, возвращаемое новым методом get_aggregate_report:
Для реализации метода get_aggregate_report мы воспользуемся представлением OrderedQty, которое, напомню, в CelestaSQL-файле выглядит так:
Запрос стандартный: мы суммируем строки заказов по количеству и группируем по коду товара. Для представления уже создался курсор OrderedQtyCursor, которым мы можем воспользоваться. Мы объявляем этот курсор, итерируем по нему и собираем нужный JSON:
Материализованные представления Celesta
Celesta старается все стандартные задачи, с которыми постоянно сталкиваются программисты бизнес-логики, реализовать на уровне платформы.
В MS SQL Server есть прекрасная концепция материализованных (индексированных) представлений, которые хранятся как таблицы и быстро обновляются по мере того, как изменяются данные в исходных таблицах. Если бы мы работали в «чистом» MS SQL Server, то для нашего случая замена представления на индексированное была бы как раз то, что надо: извлечение агрегированного отчёта не замедлялось бы по мере накопления данных, а работа по обновлению агрегированного отчёта выполнялась бы в момент вставки данных в таблицу строк заказа и также не сильно увеличивалась бы при росте числа строк.
Но мы работаем с PostgreSQL через Celesta. Что мы можем сделать? Переопределим представление, добавив слово materialized:
Запустим систему и посмотрим, что сделалось с базой данных.
Мы заметим, что представление OrderedQty исчезло, а вместо него появилась таблица OrderedQty. При этом, по мере наполнения данными таблицы OrderLine, в таблице OrderedQty будет «волшебным образом» обновляться информация, так, как будто бы OrderedQty являлось бы представлением.
Никакого волшебства тут нет, если мы взглянем на триггеры, построенные на таблице OrderLine. Celesta, получив задачу создать «материализованное представление», проанализировала запрос и создала триггеры на таблице OrderLine, обновляющие OrderedQty. Вставкой единственного ключевого слова — materialized — в CelestaSQL-файл мы решили проблему деградации производительности, а код бизнес-логики даже не потребовалось изменять!
Естественно, этот подход имеет свои, и довольно жёсткие, ограничения. «Материализованными» в Celesta могут становиться только представления, построенные на одной таблице, без JOIN-ов, с агрегацией по GROUP BY. Однако этого достаточно для того, чтобы строить, например, ведомости остатков средств по счетам, товаров по ячейкам склада и т. п. часто встречающиеся на практике отчёты.
Flute: REST-endpoints, расписания, очереди и т. п.
В конце нашего введения в Celesta остаётся обсудить, как код, который мы запускали в IDE как модульный тест «связать с внешним миром», превратить в работающий сервис.
Например, это можно сделать, взяв Maven-зависимость Celesta в ваш Java-проект и запуская нужные методы через Celesta.getInstance().runPython( ).
Но можно обойтись и вовсе без Java-программирования. У нас есть модуль, называемый Flute (он же — «Флейта»), который устанавливается как сервис на Windows и Linux. Он использует Celesta и реализует много способов, которые позволят ваши скрипты «проигрывать». Вот эти способы:
В данном случае декоратор map указывает системе Flute, с каким URL-ом будет связан данный код.
Модуль Flute конфигурируется через файл flute.xml примерно следующего содержания:
Подробное описание возможностей Flute приведено в документации.