1. Что такое Celesta?

Celesta — Java-библиотека и легковесный фреймворк, упрощающий работу с реляционной базой данных из сервисов, реализующих бизнес-логику.

Может быть использована путём подключения внешней зависимости через Maven Central, также имеет собственный Spring Boot starter.

640px Duke2 2

Celesta предоставляет:

  1. Независимость от типа базы данных. Решение, сделанное в Celesta, может переноситься между любыми базами данных поддерживаемых типов. Мы по возможности прозрачно для разработчиков решений поддерживаем следующие типы СУБД (список не окончательный):

    • MS SQL Server

    • Oracle

    • Postgre SQL

    • H2

  2. Database-first проектирование структуры данных. Проектируйте сначала таблицы, связи между ними и представления, а не классы!

  3. Автоматическую миграцию структуры базы данных на основе идемпотентного DDL.

  4. Упрощённое модульное тестирование сервисов, работающих с базой данных.

  5. Автоматически генерируемый на основе структуры БД промежуточный слой доступа к таблицам для написания бизнес-логики.

  6. Интеграцию приложения с уже сложившейся базой данных.

  7. Распределение прав доступа к таблицам.

  8. Аудит изменений, производимых в таблицах.

2. Часть 1. Быстрый старт. Maven-зависимости

Работающий тестовый пример, демонстрирующий возможности Celesta, доступен здесь: https://github.com/inponomarev/celesta-demo/.

Для создания проекта Spring Boot с помощью Maven необходимо указать следующие зависимости:

    <properties>
       <spring.boot.version>2.0.6.RELEASE</spring.boot.version>
       <!-- Проверьте свежую версию ru.curs:spring-boot-starter-web на Maven Central -->
       <spring.boot.starter.celesta.version>2.0.1</spring.boot.starter.celesta.version>
       <!-- Проверьте свежую версию ru.curs:celesta-parent на Maven Central -->
       <celesta.version>7.0.9</celesta.version>
    </properties>

. . .
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <dependency>
            <groupId>ru.curs</groupId>
            <!-- Собственный Spring Boot starter облегчает конфигурацию Celesta в Spring Boot приложениях -->
            <artifactId>spring-boot-starter-celesta</artifactId>
            <version>${spring.boot.starter.celesta.version}</version>
            <!-- Исключаем зависимость от Celesta, указанную в Celesta spring boot starter,
                 чтобы ниже указать более актуальный номер версии Celesta -->
            <exclusions>
                <exclusion>
                    <groupId>ru.curs</groupId>
                    <artifactId>celesta-system-services</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <!-- Подключаем Celesta более свежей версии, чем предлагает Spring Boot Starter-->
            <groupId>ru.curs</groupId>
            <artifactId>celesta-system-services</artifactId>
            <version>${celesta.version}</version>
        </dependency>
    ...
    </dependencies>

Также для кодогенерации классов доступа к данным понадобится ru.curs:celesta-maven-plugin. В его настройках необходимо указать путь к папке score:

    <build>
        <plugins>
            <plugin>
                <groupId>ru.curs</groupId>
                <artifactId>celesta-maven-plugin</artifactId>
                <version>${celesta.version}</version>
                <configuration>
                    <scores>
                        <score>
                            <path>${project.basedir}/src/main/resources/score</path>
                        </score>
                    </scores>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>gen-cursors</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

В папке src/main/resources/score следует разместить скрипты определения базы данных на языке CelestaSQL. Например,

-- score/ru/curs/demo/demo.sql
CREATE SCHEMA demo VERSION '1.0';

/**Заголовок счёта*/
CREATE TABLE OrderHeader(
  id VARCHAR(30) NOT NULL,
  date DATETIME,
  customer_id VARCHAR(30),

  /**Название клиента */
  customer_name VARCHAR(50),
  manager_id VARCHAR(30),
  CONSTRAINT Pk_OrderHeader PRIMARY KEY (id)
);

/**Строка счёта*/
CREATE TABLE OrderLine(
  order_id VARCHAR(30) NOT NULL,
  line_no INT NOT NULL,
  item_id VARCHAR(30) NOT NULL,
  item_name VARCHAR(100),
  qty INT NOT NULL DEFAULT 0,
  cost REAL NOT NULL DEFAULT 0.0,
  CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no)
);

ALTER TABLE OrderLine ADD CONSTRAINT fk_OrderLine FOREIGN KEY (order_id) REFERENCES OrderHeader(id);
create materialized view OrderedQty as
select item_id, sum(qty) as qty from OrderLine group by item_id;

Команда mvn verify приведёт к появлению Java-файлов OrderHeaderCursor.java и OrderLineCursor.java с классами курсоров в папке target/generated-sources/celesta/…​.

Эти классы можно использовать при создании сервисов (см. также более полный демонстрационный пример):

@Service
public class DocumentService {
    @CelestaTransaction
    public void postOrder(CallContext context, OrderDto doc) {
        try (OrderHeaderCursor header = new OrderHeaderCursor(context);
             OrderLineCursor line = new OrderLineCursor(context)) {
             . . .
    }
}

Управление настройками проекта осуществляется любым доступным для Spring Boot способом, в частности, с помощью файла application.yml.

3. Часть 2. Технические детали и настройка Celesta

3.1. Словарь основных понятий

Партитура (Score)

совокупность гранул, с которыми работает данный экземпляр Celesta.

Гранула (Grain)

функциональная гранула (схема) БД.

Имя гранулы (Grain name)

уникальный (для всех решений, когда-либо созданных на Celesta) набор латинских букв и цифр, начинающийся с буквы. Рекомендовано использование только строчных букв. Служит для идентификации гранул и разделения таблиц на пространства имён. Примеры имён: bc, skk, sor, skp. Используется, во-первых, для создания пространства имён таблиц (например, через schema в SQL Server), а во-вторых, для создания пакетов, в которых находятся классы слоя доступа к данным. Таким образом, не существует ограничения на глобальную уникальность имени таблицы среди всех гранул — достаточно уникальности имени таблицы внутри гранулы, т. е. могут одновременно существовать, например, таблицы skk.application и sor.application. Имя гранулы должно быть, по возможности, кратким: во-первых, из-за того, что имя каждой гранулы часто используется в коде, а во-вторых, из-за ограничений реализации в СУБД Oracle, суммарная длина имени гранулы и имени таблицы не может превышать 29 символов.

Скрипт создания гранулы (Grain creation script)

текстовый файл, содержащий скрипт на языке CelestaSQL. Возможна разработка и модификация скрипта гранулы с помощью программы DbSchema. Этот файл содержит информацию о

  1. таблицах, включая информацию о

    1. полях и их типах (подмножество допустимых типов выбрано таким образом, чтобы обеспечить универсальную поддержку во всех поддерживаемых типах баз данных)

    2. первичном ключе (primary key) таблицы – его наличие обязательно требуется для работы промежуточного слоя,

    3. DEFAULT-значениях на полях,

    4. ограничениях NOT NULL на полях,

  2. индексах,

  3. последовательностях (SEQUENCEs),

  4. связях между таблицами (Foreign Keys),

  5. представлениях (VIEWs).

Версия гранулы (Grain version tag)

идентификатор версии в виде перечисленных через запятую компонент, явно проставляемый разработчиком гранулы в команде CREATE GRAIN …​ VERSION …​ скрипта гранулы. Служит для защиты от непроизвольного автоматического даунгрейда базы данных при запуске старой версии гранулы на более свежей версии базы данных. Автообновление базы данных никогда не будет выполняться, если version tag в базе данных больше, чем version tag скрипта гранулы, либо если версии не согласованы.

Важно
Версия состоит из перечисленных через запятую компонент и может выглядеть таким образом: 1.23,TITAN3.34. Читать это нужно следующим образом: базовая версия 1.23, доработка для проекта TITAN – 3.34. Регулярное выражение для проверки формата компонента: ([A-Z_]*)([0-9]+\\.[0-9]+). Требуется, чтобы каждая из составляющих версии имела двухкомпонентный (и только двухкомпонентный!) формат, а префикс либо отсутствовал, либо состоял из заглавных латинских букв и знака подчёркивания. Когда система определяет возможность автоапгрейда, сравниваются все тэги версии последовательно.

В этом смысле, по отношению к тэгу «1.23,TITAN3.34»:

  • «1.23,TITAN3.35» – более свежая версия (обновилась модификация), можно выполнять автоапгрейд

  • «1.24,TITAN3.34» – более свежая версия (обновилась базовая версия), можно выполнять автоапгрейд

  • «1.23,TITAN3.34,PLUTO1.00» – более свежая версия (добавилась ещё одна модификация), можно выполнять автоапгрейд

  • «TITAN3.34,1.23» – та же самая версия (порядок следования тэгов не играет роли), автоапгрейд выполняться будет лишь при несовпадении контрольных сумм, ошибки не произойдёт

  • «1.22,TITAN3.34» – более старая базовая версия, автоапгрейд выполняться не будет, произойдёт ошибка и Celesta остановится.

  • «1.22,TITAN3.36» – несогласующаяся версия, апдейт выполняться не будет, ошибка. Версии «1.23,PLUTO1.00», «1.25» также будут несогласующимся с версией «1.23,TITAN3.34» и не станут накатываться автоматически (для самоконтроля попробуйте объяснить себе, почему).

Каждая из версий сравнивается, как число с плавающей запятой.

Контрольная сумма гранулы (Grain checksum)

автоматически вычисляемая контрольная сумма скрипта гранулы. Служит для различения гранул по структуре базы данных. Следует осознавать, что скрипты создания гранул, имеющие одинаковый version tag, могут преднамеренно (в процессе разработки) или непреднамеренно (из-за неаккуратности разработчика) иметь различное содержание. База данных, автоматически сформированная по grain creation script, помимо version tag, хранит и контрольную сумму grain creation script’а, чтобы отследить момент, когда к ней установило контакт приложение с изменённым метаописанием гранулы. Одновременное равенство version tag и grain checksum является достаточным условием для того, чтобы продолжать работу без попыток обновления структуры базы данных. Ради лёгкости проверки и открытости алгоритма контрольная сумма состоит из двух значений: длины файла скрипта (записываемой в формате десятичного числа) и его CRC32 (записываемом в виде 8 шестнадцатеричных цифр).

Score path

аналог CLASSPATH для Java, т. е. набор перечисленных через разделитель папок с наборами гранул. Папка с набором гранул — это папка, подпапки которой имеют имена гранул, и содержат скрипты создания гранул с расширением .sql.

Гранулы из папок с наборами для Celesta собираются в единый список, т. е. в процессе работы Celesta нет никакой возможности (и необходимости) определить, к какому из наборов принадлежит та или иная гранула. Каждая гранула в системе должна иметь уникальное имя, и если мы работаем всего с одним набором гранул, то уникальность имени обеспечивается файловой системой. Однако используя несколько наборов гранул через точку с запятой, мы не застрахованы от случая, когда гранула с одним и тем же именем через разные наборы входит в систему дважды. В этом случае при инициализации Celesta произойдёт ошибка. Score path задаётся в настроечном .properties-файле, см. раздел Базовая настройка Celesta.

Системная гранула celesta

особая гранула, структура таблиц которой не подлежит изменению. Таблицы этой гранулы используются системой во внутренних целях. При этом запись и редактирование данных в части из этих таблиц является частью стандартной настройки системы. Описание гранулы "celesta" см. в разделе Системные таблицы Celesta.

Таблица celesta.grains

системная таблица Celesta в базе данных. Наличие данной таблицы указывает на то, что Celesta подсоединена к «своей» базе данных, в противном случае Celesta будет пытаться развернуть базу «с нуля». Таблица содержит информацию о состоянии гранул, развёрнутых в базе. Описание полей таблицы содержится в разделе «Системные таблицы Celesta». Информация в этой таблице активно используется во время Grain startup sequence (см. далее).

Статус гранулы в базе данных (Grain database state)

сохраняемое таблице grains состояние гранулы по отношению к базе, наполненной данными. Может быть одним из следующих:

  • 0 – ready — гранула развёрнута и готова к использованию (при этом её version tag и контрольная сумма записаны в соответствующих полях таблицы grains).

  • 1 – upgrading — гранула находится в процессе создания или апгрейда другим приложением Celesta, подключённым к базе данных.

  • 2 – error — последняя попытка автообновления завершилась неудачно, в этом случае в поле message находится сообщение об ошибке

  • 3 – recover (эквивалент — отсутствие записи в таблице grains при наличии гранулы в папке score) — гранула отсутствует или нуждается в регенерации (например, после ошибки апгрейда)

  • 4 – lock — гранула не нуждается в автоматическом обновлении структуры ни при каких обстоятельствах.

Необходимость статуса upgrading определяется тем, что СУБД Oracle не поддерживает DDL-транзакций. В то же время, мы предполагаем возможность одновременного подключения нескольких серверов приложений Celesta к одной базе данных. Выставление статуса upgrading, поэтому, влияет на grain startup sequence и предотвращает конфликты при автоматическом обновлении.

Последовательность запуска гранулы (Grain startup sequence)

операции, выполняемые системой Celesta для каждой гранулы при запуске. При этом, при необходимости, происходит перегенерация классов доступа к данным, а также, при необходимости и возможности, автоапгрейд базы данных (см. далее отдельно про автоапгрейд и activity-диаграммы).

Автоапгрейд базы данных

составная часть Grain startup sequence, при которой происходит сравнение структуры существующей базы данных со структурой, заданной метаданными в Celesta при помощи скриптов создания гранул. После сравнения разница устраняется с помощью автоматически создаваемых и исполняемых CLREATE/ALTER команд.

3.2. Операции, выполняемые при инициализации Celesta

3.2.1. Общая последовательность операций

Инициализация Celesta происходит в несколько этапов, о чём сообщает вывод в stdout:

Celesta initialization: score parsing...done.
Celesta pre-initialization: system settings reading...done.
Celesta initialization: database upgrade...done.
  1. В процессе чтения системных настроек Celesta анализирует состояние базовой настройки, проверяет тот факт, что все обязательные параметры заданы, и что все параметры заданы корректно. При обнаружении ошибки в настройках запуск системы не будет продолжен.

  2. При разборе метаданных происходит чтение всех файлов *.sql в алфавитном порядке, их синтаксический анализ и построение внутренней объектной модели базы данных (метаданных). Система в папках score собирает из скриптов CelestaSQL информацию о гранулах, таблицах и полях. Вычисляются их контрольные суммы, находятся версии. При возникновении ошибок синтаксиса или несогласованности CelestaSQL-скриптов на данном этапе запуск системы прерывается.

  3. Процедура авто-миграции базы данных является наиболее сложной и затратной по времени.

3.2.2. Автоматическая миграция базы данных

На этом этапе сервер приложений соединяется с базой данных и проверяет наличие в ней системной таблицы celesta.grains. Если таблица не найдена, она автоматически создаётся (CREATE-командой), однако это происходит лишь в следующих случаях: 1) база данных совершенно пустая 2) в базовой настройке выставлено свойство force.dbinitialize (это защищает от «порчи» существующих, непустых баз данных при ошибочном присоединении к ним системы Celesta). Если в процессе проверки наличия/создания таблицы celesta.grains возникла ошибка, то генерируется фатальная ошибка и система не запускается.

Определение необходимости миграции гранулы (схемы)

Далее идёт цикл по всем доступным в метаданных гранулам (схемам) и всем доступным в celesta.grains гранулам. (При этом из процесса миграции исключаются гранулы, объявленные с опцией WITH NO AUTOUPDATE в CelestaSQL-скрипте).

  1. Если гранула есть в метаданных, но её нет в celesta.grains — выполняется апдейт по creation-скрипту (т. е. предполагается, что соответствующие таблицы в базе данных могут и присутствовать).

  2. Если гранула есть в celesta.grains, но её нет в метаданных — ничего не происходит. Автоматически гранулы из базы данных не удаляются, т. к. их таблицы могут содержать важную информацию. Просто для этих таблиц не будут сформированы классы доступа к данным.

  3. Если гранула есть и там, и там: сервер приложений находит в таблице celesta.grains запись про состояние, версию и контрольную сумму метаданных, которые «накатаны» на базу данных при последнем запуске и сравнивает с версией метаданных, которыми сервер располагает.

    • recover (3) — действуем аналогично отсутствию записи.

    • lock (4) — структура не обновляется, переходим к следующей грануле.

    • upgrading (1) — останов процесса с ошибкой “Cannot proceed with database upgrade: there are grains not in 'ready', 'recover' or 'lock' state”.

    • error (2) — останов процесса с ошибкой “Cannot proceed with database upgrade: there are grains not in 'ready', 'recover' or 'lock' state”.

    • ready (0) — продолжение процесса.

      1. Если версия и контрольная сумма совпадают — ничего не происходит.

      2. Если версия различна: если версия изменилась в сторону увеличения — апгрейд вне зависимости от контрольной суммы. Если в сторону уменьшения либо если версия несовместима (см. выше описание логики работы с version tags) — вне зависимости от контрольной суммы останов с ошибкой: “Grain '…​' version '…​' is lower than / is inconsistent with database grain version '…​'. Will not proceed with auto-upgrade.”

      3. Если версия совпадает, а контрольная сумма различна – апгрейд.

Процедура миграции гранулы (схемы).

Если на основании описанного выше алгоритма система решает, что апгрейд гранулы необходим — в грануле сбрасываются все представления и начинается цикл по таблицам в метаданных. Если таблица не найдена в базе, она создаётся. Если таблица найдена — цикл по полям. Если поле не найдено, оно создаётся (следует учесть, что добавление в непустую таблицу not null поля без значения default приводит к ошибке). Если поле найдено, и в нём не совпадает тип, default-значение или ограничение null/not null — конвертируется тип или соответствующие атрибуты поля — опять же, это возможно не всегда, в зависимости от имеющихся в таблице данных. Ошибки на данном этапе приводят к переводу гранулы в состояние error и требуют ручного вмешательства администратора базы данных. После завершения синхронизации таблиц синхронизируются внешние ключи на таблицах, и полностью синхронизируются индексы на таблицах гранулы, чтобы состав индексов в БД соответствовал составу индексов, объявленных в creation-скрипте. В самую последнюю очередь создаются представления (views).

При процедуре апгрейда не происходит удаления полей из базы данных, удаления таблиц и тем более удаления целых гранул. Если администратор базы данных уверен, что соответствующие объекты (и содержащиеся в них данные!) более не нужны, он должен проделать удаление вручную.

Тем не менее, при синхронизации будет происходить удаление внешних ключей, индексов и представлений, если их объявления были убраны из скрипта создания гранулы.

3.2.3. Полное или частичное отключение автоматической миграции

В Celesta имеется несколько способов отключить процесс автообновления:

3.3. Базовая настройка Celesta

К базовым настройкам Celesta относятся общесистемные параметры, такие как параметры подключения к базе данных и путь к партитуре (score path, см. Основные понятия).

Эти настройки в виде экземпляра Properties передаются в конструктор класса BaseAppSettings, который, в свою очередь, является параметром конструктора класса Celesta.

При использовании Celesta Spring Boot starter эти настройки транслируются через конфигурацию любым из способов способов конфигурации Spring Boot приложений, например, через файл application.yml. В последнем случае корневым ключом YAML-файла должен быть celesta, а остальные свойства записываются в иерархической структуре.

Например, для задания свойств jdbc.url, jdbc.username и jdbc.password структура файла application.yml будет следующей:

celesta:
  jdbc:
    url: jdbc:postgresql://127.0.0.1:5432/celesta
    username: foo
    password: bar

Некоторые IDE, например, IntelliJ IDEA, выполняют контекстную подсказку при редактировании файла application.yml.

Параметр Назначение Обязательный Пример значения Значение по умолчанию

score.path

score path Celesta (список папок, разделённый разделителем путей, специфичным для операционной системы)

Да

classpath:score

-

rdbms.connection.url

URL подключения к JDBC-драйверу

Да

  • jdbc:sqlserver://172.16.1.114:52836;databaseName=celesta;user=sa;password=111

  • jdbc:postgresql://127.0.0.1:5432/celesta?user=postgres&password=7464

  • jdbc:oracle:thin:celesta/123@192.168.110.128:1521:XE

-

rdbms.connection.username

Логин для подключения к базе данных

Нет (если не заполнен, используется информация из rdbms.connection.url)

-

rdbms.connection.password

Пароль для подключения к базе данных

Нет (если не заполнен, используется информация из rdbms.connection.url

-

skip.dbupdate

Заставляет систему при инициализации полностью пропустить фазу обновления базы данных (включая создание системных таблиц). Этот параметр категорически не рекомендуется включать, если вы не знаете твёрдо, чего хотите добиться.

Этот параметр требуется при некоторых сценариях разворачивания Celesta на заранее существующей базе данных - например, для работы системы Flute, если в её скриптах не планируется использовать классы Celesta для доступа к данным.

Нет

true

false

force.dbinitialize

Заставляет систему при инициализации создавать системные объекты Celesta даже в том случае, если база данных не пустая (уже содержит таблицы). Этот параметр категорически не рекомендуется включать, если вы не знаете твёрдо, чего хотите добиться, и рекомендуется выключать после того, как база данных будет проинициализирована один раз.

Этот параметр требуется при некоторых сценариях разворачивания Celesta на заранее существующей базе данных.

Нет

true

false

h2.in-memory

Значение true заставляет Celesta использовать базу H2 в in-memory режиме. Параметры JDBC подключения при этом игнорируются. Режим необходим, прежде всего, для модульных тестов.

Нет

true

false

h2.port

Если указано целочисленное значение, и при этом h2.in-memory установлено в true, то база H2 запускается в виде сервера на указанном порту. Данный режим позволяет присоединяться к базе данных H2 внешним приложениям, читать и модифицировать данные. Параметры JDBC подключения при этом игнорируются. Этот режим необходим для UI и приёмочных автоматических тестов.

Нет

true

false

h2.referential.integrity

Использовать ли контроль ссылочной целостности (контроль Foreign Keys), работая с базой H2 в in-memory режиме (для всех других БД данный параметр игнорируется). По умолчанию при работе с H2 in-memory, ссылочная целостность отключается для упрощения создания модульных тестов.

Нет

true

false

3.4. Системные таблицы Celesta

3.4.1. Структура системной схемы celesta

Помимо таблиц и гранул, определённых пользователем, система Celesta добавляет в базу данных таблицы в собственной системной грануле "celesta", структура которой представлена на диаграмме ниже.

Структура (в том числе состав полей) таблиц, относящихся к системной грануле "celesta", не подлежит изменению, а для доступа к их данным используются встроенные классы из пакета ru.curs.celesta.syscursors. Тем не менее, изменение данных в части из этих таблиц является частью штатной, необходимой настройки системы.

Системные таблицы предназначены для:

  • Координирования автоапгрейда (таблица grains).

  • Распределения прав доступа к таблицам и представлениям (permissions, roles, userroles).

  • Настройки системы логирования (logsetup) и хранения логов (log).

systemtables

Назначение таблиц:

celesta.grains

перечень гранул со статусами. Ведётся автоматически, ручные изменения разработчиком решений сводятся к выставлению статуса "recover" на грануле после неудавшейся попытки автообновления и к удалению записи об удалённой грануле.

celesta.tables

перечень таблиц и представлений. Содержимое данной таблицы автоматически синхронизируется с перечнем таблиц и представлений, доступных в гранулах Celesta, пользователю не следует вмешиваться в данные этой таблицы (в любом случае изменения будут потеряны при следующей синхронизации). Поле tabletype указывает на то, является запись таблицей ("T") или представлением ("V").

celesta.roles

перечень ролей celesta. Сюда следует вручную добавлять роли, в том числе роли с системными именами "reader", "editor" и т. д. (о ролях с системными именами см. ниже).

celesta.userroles

связь идентификаторов пользователей (логинов) с ролями. Заполняется вручную.

celesta.permissions

разрешения ролям на таблицы. Заполняется вручную.

celesta.logsetup

настройки логирования. Заполняется вручную.

celesta.log

лог изменений. Заполняется системой автоматически при каждом изменении данных, однако данные пишутся лишь по таблицам и действиям, указанным в celesta.logsetup.

3.4.2. Таблица celesta.grains

Данная таблица является важнейшей из системных таблиц Celesta, т. к. её содержимое управляет синхронизацией структуры базы данных с метаданными в момент запуска системы. Таблица содержит следующие поля:

Имя поля

Тип поля

Значение поля

id

varchar(16)

имя (код) гранулы

version

text

version tag гранулы

length

int

длина creation-скрипта гранулы в байтах (составляющая часть контрольной суммы)

checksum

int

CRC32 creation-скрипта гранулы (составляющая часть контрольной суммы)

state

int

статус гранулы: * 0 – ready — гранула развёрнута и готова к использованию (при этом её version tag и контрольная сумма записаны в полях version, length, checksum).

  • 1 – upgrading — гранула находится в процессе создания или апгрейда другим приложением Celesta, подключённым к базе данных.

  • 2 – error — последняя попытка автообновления завершилась неудачно, в этом случае в поле message находится сообщение об ошибке

  • 3 – recover (эквивалент — отсутствие записи в таблице grains при наличии гранулы в папке score) — гранула отсутствует или нуждается в регенерации (например, после ошибки апгрейда)

  • 4 – lock — гранула не нуждается в автоматическом обновлении структуры ни при каких обстоятельствах

lastmodified

datetime

дата и время последнего обновления статуса гранулы

Message

text

комментарий (например, сообщение об ошибке при последнем неудавшемся автообновлении)

3.4.3. Система распределения прав доступа

Как видно из структуры таблиц, права на таблицы раздаются ролям, прямой раздачи прав пользователям не предусмотрено.

В поле roleid указывается идентификатор роли, в полях grainid и tablename — ссылка на таблицу. В битовых полях r, i, m, d выставляются флаги, если необходимы права, соответственно, на чтение, вставку, модификацию и удаление.

Предусмотрены также специальные, системные имена ролей: reader и editor, а также роли вида <имя гранулы>.reader и <имя гранулы>.editor.

Роль reader даёт право на чтение всех без исключения таблиц, роль <имя гранулы>.reader (например, "skk.reader") даёт право на чтение всех таблиц в соответствующей грануле.

Роль editor даёт полные права (на чтение, вставку, модификацию и удаление) всех таблиц. Роль <имя гранулы>.editor (например, "skk.editor") даёт полные права на все таблиц в соответствующей грануле.

3.4.4. Система логирования

При любом изменении данных, производимых через курсоры Celesta, работает не только система распределения прав доступа, но также и система логирования изменений данных, записывая все изменения в таблицу celesta.log. Однако, чтобы celesta.log не засорялась потенциально огромным количеством ненужных данных, логируются лишь изменения на таблицах, явно указанных в таблице celesta.logsetup. Более того, имеется возможность отдельно включать логирование вставки, модификации и удаления записи.

Для того, чтобы включить логирование изменений данных таблиц, производимых через систему Celesta, необходимо занести соответствующие настройки в таблицу celesta.logsetup. При этом в полях grainid и tablename указывается ссылка на таблицу, а в битовых полях i, m, d выставляются флаги, если необходимо логирование, соответственно, вставки, модификации и удаления.

Таблица celesta.log состоит из следующих полей:

Имя поля

Тип поля

Значение поля

entryno

INT

автоинкрементируемый целочисленный номер записи в таблице лога

entry_time

DATETIME

время записи

userid

VARCHAR(250)

идентификатор пользователя, от имени которого произведено изменение

sessionid

VARCHAR(250)

идентификатор пользовательской сессии, в рамках которой произведено изменение

grainid

VARCHAR(16)

идентификатор гранулы

tablename

VARCHAR(100)

имя таблицы

action_type

VARCHAR(1)

тип действия (I для вставки, M для модификации, D для удаления)

pkvalue1

varchar(100)

значение первого поля первичного ключа (сведённое к текстовому типу)

pkvalue2

varchar(100)

значение второго поля первичного ключа (если есть, сведённое к текстовому типу)

pkvalue3

varchar(100)

значение третьего поля первичного ключа (если есть, сведённое к текстовому типу)

oldvalues

varchar(4000)

прежнее состояние записи (поля сведены к тексту и перечислены через запятую в формате CSV, информация обрезана по длине поля). Значение поля заполняется для действий M и D

newvalues

varchar(4000)

текущее состояние записи в том же формате, что и в поле oldvalues. Заполняется для действий M и I

3.4.5. Система профилирования

Для поиска и устранения проблем, связанных с быстродействием, Celesta может работать в режиме профилирования, включаемом при помощи метода setProfilemode(true) экземпляра Celesta.

В режиме профилирования информация обо всех вызовах процедур записывается в таблицу calllog, состоящую из следующих полей:

Имя поля

Тип поля

Значение поля

entryno

INT

автоинкрементируемый целочисленный номер записи в таблице лога

sessionid

VARCHAR(250)

идентификатор пользовательской сессии, в которой была запущена процедура

userid

VARCHAR(250)

идентификатор пользователя, от имени которого работала пользовательская сессия

procname

VARCHAR(250)

имя выполнявшейся процедуры (трёхкомпонентное имя процедуры Celesta)

starttime

DATETIME

время начала выполнения процедуры

duration

INT

общее время выполнения процедуры (в миллисекундах)

4. Часть 3. Проектирование базы данных в Celesta

4.1. CelestaSQL

4.1.1. Язык CelestaSQL определения объектов базы данных

Важно
Скрипты на языке CelestaSQL должны иметь кодировку UTF-8 без символа BOM.

На языке Celesta SQL пишутся скрипты определения гранул. Скрипт на языке CelestaSQL состоит из конструкций

в обязательном порядке разделённых точкой с запятой, начиная с CREATE GRAIN:

1.file

4.1.2. Комментарии в языке CelestaSQL

CelestaSQL поддерживает стандартные однострочные и многострочные комментарии, а также комментарии CelestaDoc:

-- однострочный комментарий
/*многострочный
    комментарий*/
 /**комментарий CelestaDoc*/

Обычные комментарии могут использоваться в любом месте текста, комментарии CelestaDoc — только непосредственно перед определением гранулы, таблицы, поля или индекса.

4.1.3. Идентификаторы объектов в языке CelestaSQL

Идентификаторы объектов — это, иначе говоря, имена гранул, таблиц, полей, индексов, ограничений и представлений, их синтаксис в Celesta имеет ряд строгих ограничений.

  1. В обычных СУБД дозволяется, чтобы идентификаторы объектов содержали знаки пробела и неалфавитные знаки, если эти имена заключены в обрамляющие символы, например «[]» для MS SQL Server. Т. е. в MS SQLServer допустимо, например, такое имя таблицы: [Long Table$Name]. Celesta, однако, не может поддерживать пробелы и неалфавитные знаки в идентификаторах потому, что имя каждой Celesta-таблицы должно являться именем Java-класса, а имя каждого Celesta-поля — именем Java-переменной. Поэтому идентификатор любого именованного объекта CelestaSQL должен как минимум удовлетворять правилам наименования переменной в Java, т. е. может состоять только из больших и малых букв латинского алфавита, цифр и знаков подчёркивания, при этом не может начинаться с цифры.

  2. Заключение идентификаторов Celesta в кавычки в скриптах CelestaSQL не допускается на уровне синтаксиса, т. к. практической надобности в этом нет (имена никогда не содержат пробелов). На системном уровне при формировании запросов к СУБД Celesta, однако, всегда заключает имена своих объектов в прямые кавычки ("ANSI quotes"), чтобы гарантировать, что базы данных Oracle, PostgreSQL и H2 не будут нарушать регистр букв, составляющих идентификатор.

  3. Идентификаторы в Celesta являются чувствительными к регистру, однако нельзя создавать две таблицы, имена которых отличаются только регистром.

  4. Длина любого идентификатора в Celesta не может быть больше 30 символов.

  5. Идентификаторы (префиксы) гранул, кроме того, не могут содержать в себе знаков подчёркивания. Это связано с тем, что комбинация имени гранулы со знаком подчёркивания и другим идентификатором зачастую используется во внутрисистемных целях Celesta, и запрет на использование знака подчёркивания в именах гранул необходим для исключения возможных неоднозначностей.

4.1.4. Конструкция CREATE SCHEMA (GRAIN)

С выражения CREATE SCHEMA должен начинаться любой скрипт определения гранулы. Используется следующий синтаксис (слова GRAIN и SCHEMA являются синонимами):

2.create grain

Опция WIH NO AUTOUPDATE исключает схему целиком из процесса автообновления базы данных. Как и соответствующая опция таблицы, может быть использована в ситуации, когда структура схемы находится под внешним управлением.

4.1.5. Конструкция CREATE SEQUENCE

Используется следующий синтаксис:

3.create sequence

Основными ограничениями и отличиями такого запроса от SQL-запросов в различных СУБД являются:

  1. Не поддерживается в версиях MSSQL ниже 2011

  2. Если не указано значение MINVALUE, то оно автоматически выставляется равным значению START WITH (по умолчанию 1)

  3. Если не указано значение MAXVALUE, то оно автоматически выставляется равным значению Long.MAX_VALUE

  4. Значение START WITH не обновляется для созданных ранее последовательностей, даже если было изменено в файле гранулы (связано с тем, что oracle разрешает данную операцию только через удаление и пересоздание последовательности, а идеология celesta не предусматривает удаление statefull объектов из БД)

  5. Если в грануле присутствует таблица с именем, например, A, то является недопустимым создание последовательности с именем A_seq, так как это имя зарезервировано системой

4.1.6. Конструкция CREATE TABLE

Используется следующий синтаксис:

3.create table
4.table constituent

Иными словами, в конструкции CREATE TABLE в скобках через запятую в любом порядке могут быть перечислены определения полей, определения первичных ключей или определения внешних ключей, а за скобками, возможно, перечислены опции. На практике определения всех полей идут подряд в самом начале, далее — определения составных первичных ключей (первичные ключи, состоящие из одного поля, можно определять на самом поле) и определения составных внешний ключей (опять же, внешние ключи, состоящие из одного поля, можно определять на самом поле).

Определения полей
4.field definition

field_definition (Определение поля) — конструкция, задающая тип поля, название поля, его свойства NULL/NOT NULL и DEFAULT, опционально может заканчиваться конструкцией PRIMARY KEY и/или FOREIGN KEY.

Сокращенное определение внешнего ключа (inline_fk_definition) имеет следующий синтаксис:

5.inline fk definition

Здесь table_ref — ссылка на таблицу, которая может быть однокомпонентной (если таблица, на которую указывает внешний ключ, находится в текущей грануле) или двухкомпонентной, с явным указанием имени гранулы:

6.table ref

Определение синтаксиса правил внешних ключей (fk_rules) см. в разделе Внешние ключи».

Типы данных

Используется следующая система типов данных.

CELESTA

Microsoft SQL Server

Oracle

PostgreSQL

H2

Java Type For Cursor

nteger (32-bit)

INT

INT

NUMBER

INT4

INTEGER

java.lang.Integer

Floating point (64-bit)

REAL

FLOAT(53)

REAL

FLOAT8 [= DOUBLE PRECISION]

DOUBLE

ava.lang.Double

Fixed point (decimal)

DECIMAL(p,s)

DECIMAL(p,s)

NUMBER(p,s)

NUMERIC(p,s)

DECIMAL(p,s)

java.math.BigDecimal

String (Unicode)

VARCHAR(n)

NVARCHAR(n)

NVARCHAR(n)

VARCHAR(n)

VARCHAR(n)

java.lang.String

Long string (Unicode)

TEXT

NVARCHAR(MAX)

NCLOB

TEXT

CLOB

java.lang.String

Binary

BLOB

VARBINARY(MAX)

BLOB

BYTEA

VARBINARY(MAX)

java.lang.String

Date/Time

DATETIME

DATETIME

TIMESTAMP

TIMESTAMP

TIMESTAMP

java.util.Date

DateTime With Time Zone

DATETIME WITH TIME ZONE

DATETIMEOFFSET

TIMESTAMP WITH TIME ZONE

TIMESTAMPZ

TIMESTAMP WITH TIME ZONE

java.time.ZonedDateTime

Boolean

BIT

BIT

NUMBER [check in (0, 1)]

BOOL

BOOLEAN

java.lang.Boolean

Для каждого типа поля имеется свой вариант определения:

8.int field

Вместо конструкции DEFAULT <целое число> для поля с типом INT может использоваться конструкция NEXTVAL(<sequence name>). Таким образом значение колонки будет инкрементироваться при вставке в зависимости от указанной последовательности. Стоит отметить, что можно использовать только последовательности, объявленные в той же грануле, что и таблица, к которой принадлежит колонка.

9.floating field
795px 13.decimal field
10.text field
11.blob field

Здесь <binary literal> — шестнадцатеричное представление последовательности байтов, начинающееся с 0x и не заключённые в кавычки, например: 0xFFAAFFAAFF.

12.datetime field

В качестве значения DEFAULT для поля с типом DATETIME может использоваться функция GETDATE() (текущий момент времени).

17.datetime with time zone field
13.bit field
Первичные ключи
14.primary key definition

primary_key_definition (определение первичного ключа) — конструкция, задающая состав полей, входящих в первичный ключ таблицы. Возможно в двух вариантах:

  • сокращённый вариант — когда ключевые слова “PRIMARY KEY” идут сразу после определения поля, это даёт возможность более короткой и наглядной записи в случае, когда первичный ключ состоит всего из одного поля

  • полный вариант — когда конструкция PRIMARY KEY находится в определении таблицы среди определения полей, и может содержать как одно поле, так и любое количество полей.

Ограничения:

  • Самое важное: для обеспечения работоспособности системы не допускается создание таблиц без PRIMARY KEY. (За исключением READ ONLY-таблиц).

  • В таблице может быть не более одного упоминания PRIMARY KEY, будь то сокращённое выражение в конце определения поля или составное выражение в определении таблицы.

  • Не допускается создание PK по полям с типами BLOB и TEXT.

  • Не допускается создание PK по nullable-полям.

  • Не допускается более одного вхождения одного и того же поля в определение PRIMARY KEY.

Внешние ключи
15.foreign key definition

foreign_key_definition (Определение внешнего ключа) — конструкция, задающая связь между таблицами по одному или нескольким полям. Как и в случае с конструкцией PRIMARY KEY, возможно использование в двух вариантах — сокращённом (встроенном в определение поля, связь по одному полю) и полном (перечисляется среди определений полей). Кроме того, внешний ключ можно создать вне определения таблицы при помощи конструкции "alter table add constraint foreign key":

16.add foreign key

Ограничения:

  • Foreign key, простой или составной, может ссылаться только на Primary Key, причём полностью. (В Celesta SQL не допускаются ограничения UNIQUE, поэтому из двух возможностей, стандартно предлагаемых в СУБД для Foreign Keys, остаётся только возможность ссылки на Primary Key).

  • Типы полей должны в точности совпадать (если поле – строковое, длина ссылающегося поля должна быть точно равна длине поля, на которое ссылаются).

  • Нельзя создать более одного определения Foreign Key на одном и том же наборе столбцов (частный случай – не может быть двух FK, определённых для одного и того же столбца).

fk_rules — ссылочные действия:

7.fk rules

Поддерживаемые ссылочные действия:

  • NO ACTION – запрет удаления/модификации родительской записи, если на неё есть ссылки

  • SET NULL – выставление NULL в ссылках. Внимание: естественно, использовать это действие запрещено для NOT NULL-able полей

  • CASCADE – каскадное удаление/обновление полей.

В момент создания foreign key сослаться можно только

  • на таблицу, определённую в текущей грануле выше по тексту

  • на таблицу, определённую с другой грануле.

Внимание
«Зацикленные» ссылки по foreign key (например, вида A→B→C→A) при разработке структуры базы данных в действительности нужны крайне редко и обычно говорят об ошибке проектировщика. Единственный широко применимый практически значимый пример «зацикливания» — ссылка таблицы на саму себя при организации иерархического перечня по принципу parent-child.

В Celesta не допускается создание «зацикленных» ссылок между таблицами, принадлежащими разным гранулам. Если надо организовать ссылки по кругу между несколькими таблицами внутри гранулы, то это можно сделать, воспользовавшись конструкцией "alter table add constraint foreign key".

В частности, работает следующий пример:

CREATE TABLE aa(idaa INT NOT NULL PRIMARY KEY, idc INT , textvalue nvarchar(10));
CREATE TABLE a (ida INT NOT NULL PRIMARY KEY, descr nvarchar(2), parent INT
                FOREIGN KEY REFERENCES a(ida), --ссылка таблицы на саму себя
                fff INT FOREIGN KEY REFERENCES aa(idaa) --первая часть круговой ссылки

внешний ключ, создаваемый вне таблицы:

ALTER TABLE aa ADD CONSTRAINT fk1
  FOREIGN KEY (idc) REFERENCES a(ida); --вторая часть круговой ссылки

Пример создания составного ключа, состоящего из двух полей:

CREATE TABLE empfiles(
  id INT NOT NULL PRIMARY KEY,
  user_id varchar(200) NOT NULL,
  orgtype_id varchar(255) NOT NULL,
  question_id varchar(30) NOT NULL,
  FOREIGN KEY (orgtype_id, question_id) REFERENCES schema.table(field, field)
Опции

Celesta позволяет указывать следующие опции после определения таблицы:

5.table options

Таким образом, поддерживаются следующие возможности:

  • WITH VERSION CHECK — режим по умолчанию: включение механизма отслеживания версий записей для исключения потерянных обновлений (см. раздел Защита от потерянных обновлений). Указывать данную опцию явно не требуется.

  • WITH NO VERSION CHECK — отключение механизма отслеживания версий записей. Требуется в случаях, когда нет желания вводить новое системное поле в таблицу. Возможность модифицировать данные в таблице при этом сохраняется, но возможны «потерянные обновления».

  • WITH READ ONLY — режим «только чтение». Требуется в случаях, когда данные таблицы поступают из внешних источников, а не заносятся средствами Celesta, либо если необходимо подключиться к таблице, относящейся к иному приложению, и потому нежелательно вносить в её структуру какие-либо изменения. В этом режиме механизм отслеживания версий записей отключается, а ORM-класс для доступа к таблице генерируется без методов модификации данных. Кроме того, для таких таблиц не требуется указывать первичный ключ.

  • Опция NO AUTOUPDATE, которая может применяться совместно с данными опциями, отключает таблицу от процесса автообновления базы данных. Используется в случае, когда структура какой-либо таблицы изменяется в базе данных вручную и система не должна пытаться синхронизировать структуру этой таблицы с описанием на языке CelestaSQL автоматически.

4.1.7. Конструкция CREATE INDEX

Индексы применяются для ускорения фильтрации по полям таблиц и создаются при помощи следующей синтаксической конструкции:

17.create index

Имена индексов должны быть уникальны в пределах гранулы. Все индексы в Celesta допускают неуникальные значения.

4.1.8. Конструкция CREATE VIEW

Представления (views) служат для доступа только на чтение к данным, собранным из одной или нескольких таблиц при помощи SQL-запроса SELECT. Для каждого своего представления Celesta создаёт объект-представление в базе данных, транслируя при этом SQL-запрос на языке CelestaSQL в соответствующий диалект языка SQL.

Представления создаются при помощи синтаксической конструкции

18.create view
  • <view name> (имя представления) должно быть уникальным в пределах гранулы и не совпадать с именем таблицы.

  • query (запрос) представляет собой SQL-запрос, имеющий следующий синтаксис:

800px 19.query
800px 20.from clause

Основными ограничениями и отличиями такого запроса от SQL-запросов в различных СУБД являются:

  1. Запросы строятся только на таблицах (текущей гранулы или соседних гранул). Во избежание выстраивания неэффективных конструкций невозможно построение запросов на представлениях.

  2. Конструкция SELECT * не поддерживается, и всякое поле запроса, если только это не ссылка на поле таблицы с уникальным в рамках запроса именем, должно иметь определённый и уникальный псевдоним. Это нужно для возможности однозначного создания класса-курсора, поля которого соответствуют именам столбцов запроса.

  3. Не поддерживается конструкция ORDER BY, т. к. при необходимости определённым образом отсортировать результирующий набор следует воспользоваться методом orderBy(…​) соответствующего курсора.

  4. Не поддерживается конструкция GROUP BY…​HAVING (разработка поддержки этой конструкции возможна лишь при возникновении обоснованной необходимости в данной функциональности).

  5. Не поддерживаются FULL JOIN (объединения LEFT и RIGHT joins) и CROSS JOIN (декартовы произведения таблиц). Практическая значимость FULL JOIN сомнительна. CROSS JOIN сам по себе также не бывает практически необходим, неосторожное использование может привести к проблемам производительности.

  6. Не поддерживаются конструкции UNION, WITH, любые вложенные запросы. Конструкция бизнес-приложения должна предусматривать достаточное количество таблиц с данными, чтобы в этих конструкциях не было необходимости.

Ссылка на таблицу (table_ref) имеет синтаксис

6.table ref 1

Имя гранулы указывать не обязательно, если таблица находится в той же грануле, что и текущее представление.

Терм, определяющий поле представления, имеет следующий синтаксис:

21.term

Над выражениям типа INTEGER и REAL допустимы обычные арифметические операции с обычным приоритетом: максимальный приоритет у унарного минуса, далее — умножение и деление («*», «/»), далее — сложение и вычитание («+», «-»). Над выражениями типа NVARCHAR допустима операция конкатенации «||». Операции над полями прочих типов недопустимы.

22.primary term

Ссылки на поля могут быть однокомпонентными (если они однозначно указывают на поле определённой таблицы) либо двухкомпонентными, в этом случае в качестве первой компоненты следует указывать псевдоним таблицы из конструкции FROM либо — если явный псевдоним отсутствует — имя таблицы.

Наконец, синтаксис логического выражения condition, используемого в конструкциях JOIN …​ ON и WHERE:

23.condition
24.predicate
25.aggregate

Стоит отметить, что term внутри конструкции SUM должен являться числом.

26.group by

Особенность конструкции GROUP BY в Celesta-SQL заключается в необходимости всегда перечислять в ней все неагрегатные колонки из выборки.

4.1.9. Конструкция CREATE MATERIALIZED VIEW

Материализованные представления (materialized views) служат для доступа только на чтение к агрегатным данным, собранным из одной таблицы и объединенным при помощи выражения GROUP BY. Для каждого своего материализованного представления Celesta создаёт таблицу в базе данных, модифицируемую триггерами при изменении родительской таблицы.

Материализованные представления создаются при помощи синтаксической конструкции

950px Create materialized view

Основными ограничениями и отличиями такого запроса от SQL-запросов в различных СУБД являются:

  1. Запросы строятся только на таблице текущей гранулы.

  2. Конструкция SELECT * не поддерживается, и всякое поле запроса, если только это не ссылка на поле таблицы с уникальным в рамках запроса именем, должно иметь определённый и уникальный псевдоним. Это нужно для возможности однозначного создания класса-курсора, поля которого соответствуют именам столбцов запроса.

  3. В выборке обязательно должен участвовать хотя бы один агрегатный и хотя бы один неагрегатный столбец.

  4. Неагрегатные колонки должны ссылаться на not null колонки родительской таблицы.

  5. Если в group by выражении участвует колонка типа datetime, то ее значения будут округляться в точности до дня (часы, минуты и более точные измерения отсекаются).

Materialized aggregate

4.1.10. Конструкция CREATE FUNCTION

Функции — они же параметризованные представления. Служат для доступа только на чтение к данным, собранным из одной или нескольких таблиц при помощи SQL-запроса SELECT с учетом переданных параметров.

Функции создаются при помощи синтаксической конструкции

Create function
Param definition
Param literal

Основными ограничениями и отличиями такого запроса от SQL-запросов в различных СУБД являются:

  1. Конструкция SELECT * не поддерживается, и всякое поле запроса, если только это не ссылка на поле таблицы с уникальным в рамках запроса именем, должно иметь определённый и уникальный псевдоним. Это нужно для возможности однозначного создания класса-курсора, поля которого соответствуют именам столбцов запроса.

  2. В объявлении обязательно должен иметься хотя бы один параметр.

  3. Все объявленные параметры обязательно должны использоваться.

В остальном синтаксическое выражение аналогично обычному View.

Пример функции:

CREATE FUNCTION pView2(param int,/*TEST*/ param2 varchar) AS
  select f1, f2, f3 from t1
  where f2 = $param AND f3 = $param2 AND f3 = $param2;

4.2. CelestaDoc

Аналогично тому, как в языке Java можно документировать объекты кода при помощи JavaDoc, или в языке Python использовать документирующую константу, доступную затем во время выполнения, объекты вида «гранула» (GRAIN), «таблица» (TABLE), «поле таблицы» и «индекс» (INDEX) могут быть задокументированы при помощи комментариев в специальном формате: /** …​ */ (две звёздочки после первого слэша, в отличие от одной звёздочки в простом комментарии). Эти комментарии называются CelestaDoc-комментариями (по аналогии с JavaDoc), и могут находиться в коде creation-скрипта непосредственно перед определениями соответствующих объектов, как в примере:

/**описание гранулы*/
CREATE SCHEMA test1 VERSION '1.0';

/**описание последовательности*/
CREATE SEQUENCE test_entryno;

/**описание таблицы*/
CREATE TABLE table2(
    /**описание первой колонки*/
    column1 INT NOT NULL DEFAULT NEXTVAL(test_entryno) PRIMARY KEY,
    /**описание второй колонки*/
    column2 INT
);

/**описание индекса idx1*/
CREATE INDEX idx1 ON  table2 (column2);

/**описание представления v1*/
CREATE VIEW v1 AS
  SELECT DISTINCT column2 FROM table2;

В отличие от простых комментариев, которые можно использовать в любом месте creation-скрипта, CelestaDoc-комментарии допустимо использовать только перед определением соответствующего объекта, в противном случае возникает ошибка синтаксиса.

Синтаксический анализатор прочитывает CelestaDoc, и эта информация доступна в объектах метаданных в процессе выполнения при помощи метода getCelestaDoc() (см. раздел Метаданные Celesta).

Цель комментариев CelestaDoc — снабжение объектов Celesta документацией и дополнительной метаинформацией — например, human readable названиями полей, информацией о том, как представлять поля в пользовательском интерфейсе и т. п.

Общепринятой практикой является запись в комментарии CelestaDoc информации в формате JSON-объекта. При этом CelestaDoc может содержать как обычный текст, так и JSON-объект. С помощью утилитного метода getCelestaDocJSON класса CelestaDocUtils можно извлечь первый валидный JSON-объект из CelestaDoc-строки.

5. Часть 4. Создание и тестирование методов, обращающихся к данным

5.1. Контекст вызова

5.1.1. Создание и жизненный цикл контекста вызова

Для того, чтобы можно было реализовать требования распределения прав доступа и логирования действий, любая операция над данными в Celesta производится от имени некоторого пользователя, «анонимных» операций быть не может. Поэтому любой Celesta-код выполняется в некотором контексте вызова (CallContext).

Привязка имени пользователя к роли определяет разрешения на доступ к таблицам, а также обеспечивает возможность логирования изменений, производимых от его имени.

Создание контекста вызова производится на уровне контроллера, где предполагается известным (по переданному в запрос токену, цифровой подписи или каким-либо ещё способом) имя пользователя, выполняющего операции.

Контекст создаётся с помощью конструктора

CallContext(String userId)

При отсутствии необходимости как-либо учитывать имя пользователя, а также использовать распределение прав доступа к таблицам, можно воспользоваться субклассом SystemCallContext, конструктор которого не принимает параметров. В этом случае создаётся контекст «системного пользователя», имеющего полные права доступа ко всем таблицам.

Любой созданный таким образом контекст далее проходит следующий жизненный цикл:

  1. Активация. Вызов метода activate(..), в который передаётся ссылка на объект Celesta и имя выполняемой процедуры. В этот момент открывается неявная транзакция в базе данных и начинается отсчёт времени выполнения вызова.

  2. Вызов сервисного метода. Контекст передаётся в качестве параметра в сервисный метод и используется для создания курсоров.

  3. Закрытие. Вызов метода close(), фиксирующего транзакцию и закрывающего все незакрытые курсоры.

При этом обычно разработчику не требуется выполнять активацию и закрытие контекста самостоятельно, т. к. они выполняются автоматически фреймворком Celesta.

  • При использовании Celesta Spring Boot starter активация и закрытие курсора выполняются при вызове методов сервисов, помеченных аннотацией @CelestaTransaction. Таким образом на этапе разработки достаточно передавать в них неактивированный контекст. Прокси-объект, создаваемый фреймворком Spring вокруг экземпляра сервисного класса, будет выполнять активацию, закрытие контекста и откат транзакции базы данных в случае необработанного исключения.

  • При использовании CelestaUnit в параметры тестов, имеющих тип CallContext, будет передан уже активированный системный контекст на базе Celesta, запущенной с базой данных H2 в in-memory режиме. Закрытие контекста, а также фиксация/откат изменений также обеспечиваются в CelestaUnit прозрачно для разработчика.

5.1.2. Использование контекста вызова

Каждый из методов сервисного слоя, использующих Celesta, принимает в качестве аргумента объект ru.curs.celesta.CallContext. Этот объект предназначен, в первую очередь, чтобы быть аргументом конструкторов курсоров. Однако и сам по себе контекст вызова имеет публично доступные методы и свойства, которые могут быть использованы внутри сервисного метода:

commit()

фиксирует текущую транзакцию. Вызывать данный метод требуется только в редких случаях, когда транзакцию обработки данных необходимо разбить на несколько частей. Обычно в вызове этого метода необходимости нет, т. к. транзакция фиксируется автоматически по завершении процедуры.

rollback()

откатывает текущую транзакцию.

getCelesta()

вызывает текущий экземпляр объекта Celesta. Необходимо, например, для получения информации о метаданных.

getUserId()

возвращает имя пользователя, от лица которого производятся действия.

getStartTime()

возвращает время создания контекста вызова (время начала работы Celesta-процедуры).

getDurationNs()

возвращает время жизни контекста вызова в наносекундах (необходимо использовать данный метод для точного замера времени выполнения процедур).

getProcName()

возвращает имя процедуры, которая была изначально вызвана (используется для нужд отладки).

getDBPid()

возвращает PID (process identifier) текущего соединения с базой данных (используется для нужд отладки).

5.2. Работа с данными через классы доступа к данным

5.2.1. Классы доступа и их стандартные методы

Для каждой из таблиц и представлений, объявленных в CelestaSQL, генерируются классы доступа к данным.

Каждый экземпляр класса доступа к данным (его мы также будем именовать «курсор») в каждый момент времени хранит информацию об одной записи (строке) в базе данных. Курсор можно передвигать по записям с учётом фильтров и сортировок. Если курсор создан для таблицы, его также можно использовать для вставки, модификации и удаления данных. В курсоре, созданном для представления, доступны только методы чтения записей.

cursors

На UML-диаграмме показана иерархия классов доступа к данным. В основе иерархии стоит класс BasicDataAccessor. Каждый класс курсоров наследуется от класса BasicCursor, класс Sequence от BasicDataAccessor:

Cursor

предназначен для работы с таблицами. Наследует все методы BasicCursor, а также добавляет ряд собственных методов для возможности модификации данных.

ViewCursor

предназначен для работы с представлениями, никаких собственных методов к BasicCursor не добавляет.

MaterializedViewCursor

предназначен для работы с материализованными представлениями. Наследует все методы BasicCursor, а также добавляет ряд собственных методов для возможности получения данных по первичному ключу.

ParameterizedViewCursor

предназначен для работы с функциями(параметризованными представлениями). Никаких собственных методов к BasicCursor не добавляет, однако имеет отличный от базового класса конструктор.

ReadOnlyTableCursor

предназначен для работы с таблицами, объявленными с опцией WITH READ ONLY, никаких собственных методов к BasicCursor не добавляет.

Sequence

предназначен для работы с последовательностями. Наследует все методы класса BasicDataAccessor и добавляет метод nextValue.

Ниже описываются методы класса Cursor, но те методы, которые унаследованы от BasicCursor (и могут применяться при работе с представлениями и неизменяемыми таблицами) отмечены значком [search].

Конструктор курсора

Конструктор каждого курсора принимает в себя параметр CallContext context, который, в свою очередь, выдаётся каждому скрипту при начале работы. Использование context-а позволяет работать с разными таблицами системы в одной транзакции и затем единым образом коммитить все изменения, кроме того, переменная context содержит информацию о текущем пользователе, используемую системами логирования и разграничения прав доступа.

Так, для курсоров типов Cursor, ViewCursor, MaterializedViewCursor, ReadOnlyTableCursor конструктор вызывается следующим образом.

ACursor a = new ACursor(context);
Ограничение столбцов в выборке

Очень часто в таблице определено много полей, но для нужд работы требуется лишь малая часть из них. Чтобы не передавать лишнюю информацию между базой и сервером приложений и увеличить быстродействие, курсоры можно создавать таким образом, чтобы получать из БД значения только нужных столбцов. Для этого в опциональный параметр курсора требуется передать массив названий полей, которые требуется извлекать. Поля, не указанные в этом массиве, будут принимать значение null.

Допустим, что в БД имеется заполненная данными таблица table1:

create table table1 (
  id int not null primary key,
  numb int not null,
  numb2 int,
  varr varchar(2) not null
);

Допустим, что в создаваемом разработчиком решении, нет необходимости в выборке данных из столбца varr. В этом случае при создании курсора можно указать список столбцов, которые необходимы. Создание такого курсора будет выглядеть так:

Set<String> set = Stream.of("numb", "numb2").collect(Collectors.toSet());
Table1Cursor tableCursor = new Table1Cursor(context, set);

Теперь при любом запросе данных из БД celesta не будет выбирать столбец varr, а в курсоре tableCursor поле varr всегда будет иметь значение null.

Некоторые особенности ограничения столбцов в выборке:

  1. Колонки, являющиеся частью первичного ключа, всегда будут попадать в курсор из БД, независимо от того, указаны они в списке необходимых полей или нет. Это сделано для корректной работы метода navigate курсоров при ограничении колонок.

  2. Колонки, являющиеся частью group by выражения материализованных представлений (materialized view) всегда будут попадать в курсор из БД.

  3. При передаче пустого списка полей или при его отсутствии будут выбираться все колонки.

  4. Использование столбцов с типом [BLOB-поля|blob] не изменяется. Данные из этих столбцов можно получать, как и раньше, отдельным методом.

Передача параметров в функции

Стоит отметить, что курсор ParameterizedViewCursor имеет собственную версию конструктора, принимающую набор именованных аргументов - параметров функции.

Допустим имеется таблица и функция для выборки из нее.

CREATE table t1 (
  id INT NOT NULL IDENTITY PRIMARY KEY,
  f1 int,
  f2 int,
  f3 VARCHAR (2)
);
CREATE FUNCTION pView1(p int) AS
  select sum (f1) as sumv, f3 as f3
  from t1 as t1
  where f2 = $p
  group by f3;

Тогда для создания курсора для функции с параметром p = 5 необходимо выполнить следующий код.

PView1Cursor pView1 = new PView1Cursor(context, 5);

Передачу параметров функции можно комбинировать с ограничением полей выборки: для этого сначала надо в обязательных аргументах курсора перечислить параметры, а последним — опциональным — аргументом передать список полей, которые вы хотите выбрать.

Изменение полей курсора

По количеству объявленных полей в классе курсора имеются атрибуты, позволяющие читать и записывать информацию в поля курсора. Так, если таблица foo определена следующим образом

CREATE TABLE foo (
  a INT NOT NULL PRIMARY KEY,
  b VARCHAR(10),
  c DATETIME,
  d BIT
)

то для того, чтобы вставить запись в таблицу foo, можно использовать следующий код:

FooCursor foo = new FooCursor(context);
foo.setA(1);
foo.setB("text");
foo.setC(new GregorianCalendar(year, month, day).getTime());
foo.insert();

Обратите внимание на использование класса Date для записи значений даты. При необходимости заполнить поле "с" текущей датой и временем, это можно было бы сделать при помощи выражения

foo.setC(new Date());

То, как изменить значение BLOB-поля, описано в статье BLOB-поля.

Отдельный атрибут getRecversion() в курсоре существует для значения системного поля recversion, необходимого для механизма защиты от потерянных обновлений.

Каждый курсор имеет следующие методы ([search] обозначены методы, унаследованные от BasicCursor, которые могут применяться при работе с представлениями и таблицами «только на чтение»):

Закрытие курсора
  • [search] close() — закрытие курсора. Данный метод высвобождает все JDBC-ресурсы, аллоцированные во время существования курсора. Обращение к методам закрытого курсора приведёт к ошибке. Данный метод вызывать не обязательно, т. к. он вызывается автоматически после выполнения Celesta-процедуры на всех курсорах, созданных во время её выполнения. Вообще, предпочтительной практикой программирования является создание как можно меньшего числа курсоров в процедуре и повторное их использование. Тем не менее, если есть необходимость в создании большого числа курсоров, то возникает необходимость и в использовании метода close() в тот самый момент, когда экземпляр курсора становится ненужным.

Методы переходов по записям
  • tryGet(Object…​ values) Осуществляет поиск записи по ключевым полям, возвращает true, если запись найдена, и false, если записи с таким первичным ключом нет в таблице. В аргументах этого метода должны быть перечислены значения полей первичного ключа, количество аргументов должно быть равно количеству полей первичного ключа таблицы. ВНИМАНИЕ! Данный метод не учитывает никаких фильтров, наложенных на таблицу. Если вам необходимо найти запись с учётом фильтров, то используйте метод [try]First.

  • get(Object…​ values) То же, что tryGet, но выбрасывает исключение, если запись не найдена.

  • [search] navigate(command) — осуществляет переход по записям относительно текущего положения. Возвращает True, если переход удачный, и False — если записи не оказалось. При этом строка command может представлять собой произвольный набор из следующих символов, каждый из которых выполняется до тех пор, пока запись не нашлась:

    • - (минус) — переход к первой записи, удовлетворяющей условиям сортировки и фильтрации,

    • + (плюс) — переход к последней записи,

    • > — переход к следующей записи, относительно текущей, удовлетворяющей условиям сортировки и фильтрации,

    • < — переход к предыдущей записи, = — обновление текущей записи, если она попадает в текущий фильтр.

  • [search] tryFirst() — то же, что navigate('-').

  • [search] first() — то же, что tryFirst(), но вызывает ошибку, если запись не найдена.

  • [search] tryLast() — то же, что navigate('+').

  • [search] last() — то же, что tryLast(), но вызывает ошибку, если запись не найдена.

  • [search] next() — то же, что navigate('>').

  • [search] previous() — то же, что navigate('<').

  • [search] tryFindSet() — открывает на сервере набор записей (recordset) и устанавливает курсор в его начало. Возвращает True, если открывшийся набор не пуст, False — если записей в наборе нет.

  • [search] findSet() — то же, что tryFindSet(), но вызывает ошибку в случае, если переход неудачен.

  • [search] nextInSet() — переход к следующей записи в текущем наборе данных на сервере. Если набор не открыт, вызов этого метода эквивалентен вызову tryFindSet(). Возвращает True, если переход состоялся, False — если достигнут конец набора.

  • [search] iterator() — возвращает итератор, позволяющий осуществить полную итерацию по набору записей с первой до последней. Например, если переменная rec содержит экземпляр курсора, то полная итерация с использованием метода iterate() может быть осуществлена следующим образом:

 for (FooCursor r: rec): {
         /* здесь внутри цикла всё,
          что вы хотите сделать с записями r */
 }

что будет полностью эквивалентно следующему коду:

if (cursor.tryFindSet()) {
    while (cursor.nextInSet()) {
        //цикл
    }
}
Замечание
В чём разница между [try]First() и [try]FindSet()? — Разница в отправляемом на БД запросе. [try]First() (а также navigate(), next(), last()…​) выполняет запрос вида SELECT TOP 1, запрашивают одну запись и сразу закрывают на сервере recordset. Метод findSet() открывает на сервере recordset и держит его для того, чтобы его можно было бы обойти при помощи метода iterate().
Замечание
Чем navigate("=") отличается от tryGet(_currentKeyValues())? — Метод navigate() учитывает текущие фильтры, а get() — не учитывает. Запись с текущим значением первичного ключа может не попасть в фильтр, поэтому navigate('=') может вернуть False в ситуации, когда tryGet(_currentKeyValues()) возвращает True.
Замечание
Что значит navigate("⇒<")? — Эта команда предписывает следующий алгоритм: "Попытайся найти текущую запись. Если запись нашлась, выйди и верни True. Если записи уже нет (удалили), сходи вперёд. Если запись нашлась, выйди и верни True. Если впереди ничего нет, сходи назад. Если запись нашлась, верни true, если нет — false.
Методы сортировки и фильтрации
  • [search] setRange(String name) Сброс любого фильтра на поле.

  • [search] setRange(String name, Object value) Установка диапазона из единственного значения на поле. Передача значения null в качестве аргумента приводит к установке фильтра 'IS NULL' на данное поле.

  • [search] setRange(String name, Object valueFrom, Object valueTo) Установка диапазона «от..до включительно» на поле (на уровне языка SQL соответствует оператору BETWEEN). Использование null в качестве аргумента не допускается.

  • [search] setFilter(String name, String value) Установка сложного фильтра на поле, описание выражений сложных фильтров приведено ниже.

  • [search] setComplexFilter(String value) Установка сложного фильтра на таблицу. Аргумент соответствует условию WHERE на языке CelestaSQL.

  • [search] setIn(BasicCursor auxiliaryCursor) Установка фильтра с вложенным запросом.

  • [search] limit(long skip, long limit) Установка ограничений на возвращаемый диапазон строк. В качестве параметров должны быть неотрицательные целые числа. Параметр skip означает количество строк, которое будет пропущено перед тем, как начнётся выдача (skip = 0 — выдача с самого начала), limit — максимальное число возвращаемых строк, при этом limit = 0 означает возврат всех строк. Вызов limit(0, 0) сбрасывает ограничения на возвращаемый диапазон набора строк. Ограничения, установленные методом limit(), не учитываются при вызове метода count().

  • [search] orderBy(String…​ names) Установка сортировки. Параметры — перечень имён полей для сортировки. Чтобы указывать сортировку по возрастанию или по убыванию, после каждого имени поля допускается добавлять ключевые слова ASC и DESC (ключевые слова могут быть в любом регистре, могут быть отделены от имени поля одним или несколькими пробелами). Если ключевое слово ASC/DESC не указано явно, сортировка идёт по возрастанию. Допускается вызов orderBy() без аргументов, чтобы сбросить все установленные ранее сортировки на сортировку по умолчанию. Имя поля можно указать не более чем в одном из аргументов метода orderBy(…​).

Следует помнить, что в Celesta не бывает не отсортированных наборов данных: по умолчанию наборы данных в Celesta всегда сортируются по полям первичного ключа, а к любому набору полей, заданному через orderBy(…​), Celesta автоматически добавляет в конец те поля первичного ключа, которые не были перечислены в аргументах. Для представлений и «WITH READ ONLY» таблиц, у которых поля первичного ключа не заданы, Celesta использует для сортировки по умолчанию первое поле. Всё это реализовано для того, чтобы итерация по записям курсора была детерминированной.

Методы инициализации
  • [search] reset() Сброс фильтров и сортировки, с сохранением значений полей буфера.

  • [search] clear() Сброс фильтров, сортировки и полная очистка буфера, включая ключевые поля.

  • init() Очистка всех полей буфера, кроме ключевых.

Методы клонирования
  • [search] copyFiltersFrom(BasicCursor c) Перенос значений всех фильтров, включая значения limit (skip и limit), из курсора с тем же типом в текущий курсор.

  • [search] copyOrderFrom(BasicCursor c) Перенос настроек сортировки из курсора с тем же типом в текущий курсор.

  • [search] copyFieldsFrom(BasicCursor c) Перенос значений всех полей из курсора с тем же типом в текущий курсор.

Методы модификации данных
  • insert() Вставка содержимого курсора в БД. При этом если запись с таким первичным ключом уже существует, возникает ошибка.

  • tryInsert() Вставка содержимого курсора в БД. true если получилось, false если запись с таким первичным ключом уже существует

  • update() Сохранение содержимого курсора в БД, выбрасывая исключение в случае, если запись с такими ключевыми полями не найдена.

  • tryUpdate() Сохранение содержимого курсора в БД, true если получилось, false если запись с таким первичным ключом не существует.

  • delete() Удаление текущей записи.

  • deleteAll() Удаление всех записей, попадающих в фильтр. NB. Триггер onDelete при этом не вызывается.

Вспомогательные методы
  • [search] canRead(), canInsert(), canModify(), canDelete() Возвращает булевское значение, указывающее на наличие прав у текущей сессии на выполнение соответствующей операции.

  • [search] count() Возвращает количество записей в отфильтрованном наборе. В частности, если фильтров на курсор не установлено, возвращает полное количество записей в таблице. Ограничения на набор записей, установленные методом limit(), не учитываются при вызове метода count().

  • [search] callContext() Возвращает контекст вызова, на котором создан данный курсор.

  • [search] _tableName() Возвращает имя таблицы или представления.

  • [search] meta() Возвращает описание таблицы или представления (метаинформацию, экземпляр класса ru.curs.celesta.score.Table/View).

  • [search] asCSVLine() Возвращает значение полей курсора в виде CSV-строки с разделителями-запятыми.

  • getMaxStrLen(String name) Возвращает длину текстового поля (в символах). Выбрасывает ошибку, если в параметр передано имя несуществующего или не текстового поля. Возвращает -1, если поле определено как NVARCHAR(MAX).

  • getXRec() Возвращает копию буфера, содержащую значения, полученные при последнем чтении данных из базы.

Важно
Обратите внимание, что методы get, first, insert, update имеют два варианта: без приставки try (просто get(…​) и т. д.) и с приставкой try (tryGet(…​), tryFirst() и т. д.). Методы без приставки try вызывают исключение, если в базе данных нет подходящих данных для выполнения действия. К примеру, first() вызовет исключение, если в установленный на курсор фильтр не попадёт ни одной записи (или, в вырожденном случае, если таблица окажется пуста). Методы get и update вызовут исключение в случае отсутствия соответствующей записи, а метод insert — если запись с таким набором значений полей первичного ключа уже существует. В то же время методы с приставкой try исключения не вызывают, а вместо этого возвращают булевское значение, сигнализирующее об успешности или неуспешности соответствующей операции.
Важно
Правильной практикой при разработке кода бизнес-логики является использование методов БЕЗ приставки try везде, где это возможно. Таким образом создаётся «самотестирующийся» код, вовремя сигнализирующий об ошибках в логике и/или в данных базы данных. К примеру, если при разработке процедуры мы предполагаем, что если приложение работает верно, то в переменной idFoo содержится идентификатор записи, существующей в таблице foo, то для получения самой записи следует писать foo.get(idFoo). В этом случае, если где-то в программе есть ошибка, приводящая к тому, что idFoo может принимать значение несуществующего идентификатора, об этом будут проинформированы разработчики и пользователи в самый момент возникновения данной ситуации. «Маскировка» возможных проблем путём использования try…​-метода без явной нужды в возвращаемом значении этого метода является грубой ошибкой, которая приведёт к общему запутыванию отладки и дестабилизации кода. Разумеется, иногда в коде нужно выяснить — есть ли запись с таким идентификатором? Для этого — и только для этого — предназначен tryGet, аналогичное справедливо для других «try-методов», использование которых в подавляющем большинстве случаев оправдано только если предполагается явное использование возвращаемых значений этих методов.

5.2.2. Использование метода setFilter

В большинстве практических случаев фильтрацию курсоров по значению поля можно выполнять при помощи методов setRange(…​) с двумя или тремя параметрами, отфильтровывающих значения по условию вида «поле = значение» либо по условию вида «поле between значение1 and значение2».

В случаях, когда простого сравнения или условия between недостаточно, метод setFilter позволяет наложить сложное логическое условие на значения в одном из полей курсора. Первым аргументом метода setFilter является имя поля, а вторым — выражение фильтра.

Правильное выражение фильтра может состоять из:

  • числовых либо текстовых литералов (в зависимости от типа поля),

  • литерала null,

  • логических операторов &, |, !,

  • операторов сравнения <, >, ..,

  • группирующих скобок (, ),

  • специальных операторов @ и % для текстовых полей.

Выражение фильтра не может быть null или пустой строкой, для сброса фильтра с поля следует вызывать метод setRange с одним параметром. Пробелы между литералами и операторами игнорируются. Выражение фильтра напрямую, без предварительной оптимизации, транслируется в условие для выражения WHERE языка SQL.

Выражения фильтра для полей с типами BIT и BLOB

Для полей с типами BIT и BLOB допустимо использование выражения фильтров вида null и !null, отфильтровывающие значения «поле is null» и «not (поле is null)»:

01.simple filter

Иные виды фильтрации для типа BLOB смысла не имеют, а для битового типа условие на True или False накладывается с помощью метода setRange(…​).

Выражения фильтра для полей с типами INTEGER и REAL

Для полей с типами INTEGER и REAL допустимо использование выражений фильтров по следующим синтаксическим правилам:

02.filter

Здесь

  • & — знак логического И,

  • | — знак логического ИЛИ,

  • ! — знак логического НЕ,

  • (, ) — группирующие скобки.

Выражение term для числовых полей имеет следующий синтаксис:

03.numeric term

Например, выражение фильтра

(10|<5)&>0

для поля с именем "foo" будет переведено в условие

("foo" = 10 or "foo" < 5) and "foo" > 0

Знаки ">" и "<", естественно, задают условия «строго больше» и «строго меньше», а использование символа ".." позволяет задавать условия «больше или равно» и «меньше или равно». Так, фильтр

.0|5..7|10..
будет транслирован в условие
"foo" <= 0 or "foo" between 5 and 7 or "foo" >= 10

(напоминаем, что оператор between в SQL задаёт диапазон с включением границ).

Важно
Обратите внимание, что выражение фильтра требует явной группировки скобками разных логических операторов, т. е. корректными являются выражения
(10|<5)&>0
10|(<5&>0)
10|<5|>0

но вызовет ошибку выражение

10|<5&>0

в котором нет группирующих скобок, явно указывающих на последовательность вычисления операторов ИЛИ и И.

Выражения фильтра для полей с типом DATETIME

Выражения фильтра для полей с типом DATETIME имеют такой же синтаксис, что и для числовых полей, но вместо числового нумерала <numeric literal> следует использовать нумерал даты в виде 'YYYYMMDD' (апостроф, восемь цифр, апостроф). Таким образом, правильные выражения фильтров для поля даты выглядят так:

'20131124'
'20131124'..'20151211'|'20111111'
(>'20131124'&..'20151211')|'20111111'..

Разные типы СУБД по-разному работают с литералами дат, но Celesta транслирует выражения фильтра в условия, корректно отрабатывающиеся каждой из поддерживаемых СУБД.

Выражения фильтра для полей с типом VARCHAR(n) и TEXT

Выражения фильтра для текстовых полей в целом похожи на выражения фильтра для числовых полей, с той лишь разницей, что вместо чисел в выражениях термов надо указывать строковые литералы в одинарных кавычках. Например, на текстовом поле корректным является фильтр 'aa'|'bb'|'cc', который отфильтрует записи, в которых значения фильтруемого поля равны "aa", "bb" или "cc". При этом, если нужно отфильтровать текст, содержащий одинарную кавычку, то её в текстовом литерале (как и обычно в языке SQL) следует удвоить: для отбора значений "John’s company" следует писать 'John''s company'. Как и все прочие типы полей, текстовые поля можно фильтровать по значению null/ not null при помощи термов null/!null.

Кроме того, текстовые поля можно фильтровать при помощи оператора LIKE, применяя специальный символ %, означающий любую комбинацию любых символов, а также при помощи специального символа @ указывать на независимость фильтра от регистра.

Более точно, синтаксические правила термов фильтра для текстовых полей выглядят следующим образом:

04.text term

Так, выражение

('aaa'&'bb')|(!'ddd'&!null)

будет транслировано в

(("foo" = 'aaa' and "foo" = 'bb') or (not ("foo" = 'ddd') and not ("foo" is null))

(что естественно никогда не будет выполнено, этот и следующий примеры даны лишь для иллюстрации принципа трансляции фильтров в язык SQL).

Выражение

@'q'|@..'cC'|@'Ff'..|@'a'..'b'|@%'5a'|'abc'%|! @ %'ef'%|null

использующее знаки @, транслируется в

UPPER("foo") = 'Q' or UPPER("foo") <= 'CC' or UPPER("foo") >= 'FF' or UPPER("foo") between 'A' and 'B'
or UPPER("foo") like '%5A' or "foo" like 'abc%' or not (UPPER("foo") like '%EF%') or "foo" is null

5.2.3. Использование метода setIn

Метод setFilter позволяет фильтровать записи, некоторое поле которых принимает любое значение из заранее заданного набора. К примеру,

myCursor.setFilter("city_id", "'MSK'|'LON'");

отфильтровывает записи, поле «код города» которых принимает значение MSK или LON. Вызов

myCursor.setFilter("city_id", "'M'%");

отфильтровывает записи, код города в которых начинается с латинской буквы M.

Однако функциональности setFilter бывает недостаточно: что если необходимо отфильтровать записи, относящиеся к городам, полное название которых на русском языке начинается с буквы «Ф»?

Одним из способов решения такой задачи могло быть следующее: отфильтровать справочник городов по city.setFilter('name', "'Ф'%"), далее выгрузить из базы данных в память полный набор идентификаторов таких городов, объединить их в строку фильтра через вертикальную черту, и использовать это как фильтр на другом курсоре. Ясно, что такой подход плох, если попадающих в фильтр записей слишком много: это породит обмен лишними данными по сети и слишком длинный SQL-запрос к интересующей нас таблице.

Для этого случая применяется метод setIn, который позволяет установить фильтр с вложенным запросом по указанному набору полей. Доступен для наследников классов Cursor и ViewCursor.

Общая схема работы с setIn такова:

устанавливаются фильтры на целевом и вспомогательных курсорах, устанавливается связь полей между целевым и вспомогательными курсорами. Связь полей задается при помощи вспомогательного класса FieldsLookup, возвращаемого в качестве результата из метода setIn целевого курсора. Метод setIn принимает в качестве единственного аргумента объект вспомогательного курсора, по которому ищется пересечение. Подготовка целевого курсора и аккумулирование пар столбцов с последующей установкой фильтра происходит следующим образом:

TargetCursor a = new TargetCursor(context);
AuxiliaryCursor b = new AuxiliaryCursor(context);
b.setRange("foo", "bar");
lookup = a.setIn(b).add("a1", "b1").add("a2", "b2");

Для данного примера в PostgreSQL, например, для доступа к строкам курсора a будет сгенерировано следующее sql выражение:

SELECT ... FROM Target WHERE ( a1, a2 ) IN (SELECT b1, b2 FROM Auxiliary WHERE Auxiliary.foo = 'bar' )

К целевому курсору можно применить любое число вспомогательных курсоров через метод and класса FieldsLookup. При этом вспомогательные курсоры между собой никак не пересекаются. Пример задания нескольких вспомогательных курсоров ниже:

TargetCursor a = new TargetCursor(context);
a.setRange("afoo", "aBar");
AuxiliaryCursor b = new AuxiliaryCursor(context);
b.setRange("bFoo", "bBar");
Auxiliary2Cursor c = new Auxiliary2Cursor(context);
c.setRange("cFoo", "cBar");
FieldsLookup lookup = a.setIn(b).add("a1", "b1").add("a2", "b2");
FieldsLookup lookup2 = lookup.and(c).add("a1", "c1");

Для данного примера в PostgreSQL, например, для доступа к строкам курсора a будет сгенерировано следующее sql выражение:

SELECT ...
FROM Target
WHERE aFoo = 'aBar'
    AND ( a1, a2 ) IN (SELECT b1, b2 FROM Auxiliary WHERE Auxiliary.bFoo = 'bBar' )
    AND (a1) IN (SELECT c1 FROM Auxiliary2 WHERE Auxiliary2.cFoo = 'cBar' )

У данного фильтра имеется набор ограничений, несоблюдение которых приведёт к выбрасыванию исключения во время выполнения методов FieldsLookup.add или BasicCursor.setIn:

  • Типы данных у каждой пары сопоставляемых полей должны в точности совпадать.

  • В каждой из таблиц должен существовать индекс, включающий в себя все столбцы из набора сопоставляемых столбцов: для примера выше для таблицы Target должен иметься индекс I1(a1, a2,..), для Auxiliary - I2(b1, b2,…​).

  • Для курсоров на таблицы соответствующие индексы должны содержать сопоставляемые столбцы в своём начале. Для нашего примера, если имеются индексы I1(a1, a2,..), I2(b1, b2,…​), следующий код вызовет исключение, т. к. поля a2, b2 находятся не в начале индексов I1 и I2:

  • Класс FieldsLookup может принять в себя курсоры только одного происхождения, т.е. либо оба курсора для работы с таблицами, либо оба курсора для работы с представлениями.

FieldsLookup lookup = setIn(b).add("a2", "b2");

5.2.4. Класс Sequence

Класс Sequence позволяет работать с последовательностями. В отличие от остальных классов доступа при кодогенерации вместо суффикса Cursor используется суффикс Sequence. Класс Sequence имеет единственный метод nextValue, позволяющий получить следующее значение последовательности в виде типа long.

Ниже приведен пример использования класса доступа Sequence:

CREATE SCHEMA sequences version '1.0';
CREATE SEQUENCE idNumerator START WITH 3;
IdNumeratorSequence sq  = new IdNumeratorSequence(ctx);
//выводит следующее значение, начиная с 3.
System.out.println(sq.nextValue());

5.2.5. Распределение прав доступа и протоколирование изменений

Работа с данными через классы доступа к данным даёт не только возможность писать универсальный, не зависящий от используемой СУБД код, но также и решить задачу централизованного распределения прав доступа к данным таблиц и протоколирования изменений.

Вызов ряда методов требует наличия соответствующих прав у пользователя на таблицы, прописанных в системных таблицах celesta.userroles и celesta.permissions, в противном случае возникает исключение PermissionDeniedException с сообщением вида "There is no …​ permission for user …​ on object …​".

Если протоколирование изменения таблицы настроено в таблице celesta.logsetup, то вызов некоторых методов будет приводить к созданиям записей в таблице celesta.log

Метод

Требуемые права

Протоколирование изменений

*

[try]first(), [try]get(), next().

требуют право на чтение (r)

не протоколируются

try]insert()

требуют право на вставку (i)

протоколируется, если включён флаг i.

  • oldvalues - пустое значение.

  • newvalues - вставляемая запись.

[try]update()

требуют право на модификацию (m)

протоколируется, если включён флаг m.

  • oldvalues - состояние записи до модификации.

  • newvalues - состояние записи после модификации.

delete[All]()

требуют право на удаление (d)

delete() протоколируется, если включён флаг d.

  • oldvalues - состояние записи до удаление.

  • newvalues - пустое значение.

deleteAll() не протоколируется и триггеры не выполняются.

Прочие методы не требуют никаких прав доступа к таблице и вызов их не протоколируется. Т. е. определить курсор, выставить на нём фильтры и даже подсчитать количество подпадающих под фильтры записей при помощи метода count() можно, даже не имея никаких прав на чтение таблицы.

5.2.6. BLOB-поля

BLOB-поля позволяют хранить в ячейке таблицы большие объёмы данных — например, целые файлы с документами. Работа с этими полями через курсор отличается от работы с полями других типов.

  1. Атрибуты курсора, соответствующие BLOB-полям, должны принимать значения со специальным типом BLOB, и никак иначе. Присвоение этим атрибутам строк и значений иных типов приведёт к ошибке при попытке вызова методов insert() или update().

  2. Получить экземпляр класса BLOB и присвоить его атрибуту курсора можно только одним способом: вызвав в курсоре метод calc<имя поля>(). Например, если BLOB-поле имеет название foo, то соответствующий метод курсора называется calcfoo(), и после его вызова будет заполнен атрибут foo.

  3. В отличие от полей других типов, которые автоматически заполняются данными таблицы при чтении записи методами get(), next() и т. п., BLOB-поля при выполнении этих методов не заполняются, а всегда получают значение null. Чтобы прочитать BLOB-поле, необходимо вызвать метод calc…​(). При этом, если курсор указывает на существующую в таблице запись, вызов метода calc…​() приведёт к чтению содержимого BLOB-а из базы данных в оперативную память. Поэтому calc…​() следует вызывать лишь тогда, когда есть намерение прочитать или изменить содержимое BLOB-а.

  4. После вызова метода calc<имя поля>(), соответствующий атрибут курсора инициализируется объектом с типом BLOB. Методы этого объекта позволяют читать и изменять его содержимое и описаны далее. После изменения содержимого объекта BLOB можно воспользоваться методом update() или insert().

  5. Если BLOB необходимо стереть из базы данных, записав в соответствующую ячейку значение NULL, нужно воспользоваться методом setNull() объекта BLOB, а затем вызвать update(). Присвоение атрибуту значения null, в отличие от полей других типов, не сработает, т. к. будет проинтерпретировано системой, как если бы BLOB не был прочитан из базы, и по update() ничего не изменится.

Методы класса BLOB:

getInStream()

возвращает экземпляр класса java.io.InputStream, из которого можно прочитать содержимое курсора или null, если в ячейке таблицы базы данных содержится значение NULL. Допускается многократный вызов этого метода, при этом всякий раз будет создаваться новый поток, читающий с начала.

getOutStream()

стирает все данные BLOB-а в памяти (если они были) и создаёт экземпляр java.io.OutputStream, в который можно записать данные для BLOB-а. Следует подчеркнуть, что каждый вызов getOutStream() удаляет из BLOB-а в памяти все данные, даже если в полученный поток ничего не будет записано. Также следует подчеркнуть, что этот метод меняет данные только в представлении BLOB в оперативной памяти, реальная запись в базу данных происходит только после вызова методов insert() или update() на курсоре.

setNull()

устанавливает для BLOB-а значение NULL.

isModified()

возвращает true, если первоначальные данные объекта были изменены вызовами getOutStream() или setNull().

size()

возвращает размер внутренних данных BLOB-а в байтах.

Примеры кода для работы с BLOB-полем:

OrderLineCursor line = new LineCursor(context);
//Далее подразумевается, что line.dat — BLOB-поле
. . .
//Пример записи
line.calcDat();
try(OutputStreamWriter osw = new OutputStreamWriter(
    line.getDat().getOutStream(), StandardCharsets.UTF_8)){
    osw.append("hello, blob field!");
}
. . .
//Пример чтения
line.calcDat();
InputStream ins = line.getDat().getInStream();
//Помним о том, что в поле может быть NULL
if (Objects.nonNull(ins)){
    try ( BufferedReader inr = new BufferedReader(
        new InputStreamReader(ins, StandardCharsets.UTF_8))) {
        //В консоль будет выведено содержимое BLOB-поля,
        //например, 'hello, blob field!'
                System.out.println(inr.readLine());
    }
}

5.2.7. Option-поля

Довольно часто в реляционных базах для бизнес-приложений необходимо иметь дело со случаем, когда какое-либо поле может принимать лишь несколько значений из фиксированного списка. Например, может оказаться так, что поле "state" в вашей таблице может принимать лишь значения "new, processing, finished, error", и никакие иные.

Т. к. список фиксированный, делать отдельный справочник и внешний ключ при этом не представляется разумным. Более того, для оптимизации объёма таблицы и скорости обработки, часто имеет смысл применять целочисленные поля, присваивая определённый "смысл" целочисленным значениям, например, так:

  • 0 - new

  • 1 - processing

  • 2 - finished

  • 3 - error

Celesta поддерживает упрощённую работу с такими полями.

Чтобы объявить, что поле может принимать лишь значения из определённого списка, необходимо прописать свойство option в CelestaDoc целочисленного или текстового поля. Например, так:

create table foo
  ...
  /**целочисленное поле статуса
  {option: [new, processing, finished, error]}*/
  state int,
  /**текстовое поле статуса
  {option: [created, closed]*/
  state2 varchar(6)

При компиляции класса доступа к данным Celesta прочитывает свойство option и генерирует дополнительный код, упрощающий использование значений из списка.

Например, для нашей таблицы foo будут автоматически созданы два вложенных класса в классе fooCursor:

public static final class State {
    public static final Integer new = 0;
    public static final Integer processing = 1;
    public static final Integer finished = 2;
    public static final Integer error = 3;
    private State() {}
}
public static final class State2 {
    public static final String created = "created";
    public static final String closed = "closed";
    private State() {}
}

Обратите внимание: для целочисленного поля возможные варианты автоматически нумеруются, а для текстового поля текстовые значения возможных вариантов буквально совпадают с их именами. Разработчик решения теперь может ссылаться на варианты значений следующим образом:

FooCursor foo = new FooCursor(context)
foo.setRange("state", FooCursor.State.finished)
if (foo.state2 == FooCursor.State2.closed){
    ....
}

5.3. Триггеры

Триггером называется написанная создателем решения функция, принимающая в качестве аргумента экземпляр курсора, присоединённая к классу курсора, автоматически вызываемая при вставке, удалении и модификации записи (иначе говоря, триггер имеет тип Consumer<YourCursor>).

При действиях insert(), update() и delete() система вызывает определённые создателем решения pre- (выполняемые до модификации данных в базе) и post- (выполняемые после модификации данных в базе) триггеры. Таким образом, всего существует шесть типов триггеров:

pre-триггеры

post-триггеры

onPreInsert

onPostInsert

onPreUpdate

onPostUpdate

onPreDelete

onPostDelete

Важно
Чаще всего имеет смысл пользоваться pre-триггерами, чтобы выполнить некоторые действия до того, как изменение будет внесено в базу. Однако обратите внимание: т. к. триггер onPreInsert выполняется до отправки содержимого курсора в базу данных, то на момент его выполнения не заполняются значения полей, обладающих свойствами DEFAULT или GETDATE(). Для их автозаполнения средствами БД следует присваивать им значение null. В триггере onPostInsert эти поля уже будут заполнены.

В триггере onPreUpdate удобно воспользоваться объектом xRec, чтобы определить, какие именно поля собираются быть изменёнными в таблице. В триггере onPostUpdate объектом xRec воспользоваться уже нельзя, т. к. он становится равным текущему буферу.

Триггеров каждого типа на одной таблице может быть сколько угодно. Триггер для любой таблицы может быть определён с помощью статических методов onPreInsert, onPreDelete и т. д. на классах курсоров. Определение триггеров с помощью статических методов делает их «глобальными», т. е. выполняющимися при любом взаимодействии с таблицей.

Т. к. метод регистрации триггера требует указания экземпляра класса Celesta, в Spring-приложениях для регистрации триггеров удобно использовать метод @PostConstruct:

@Service
public class MyService {

    @Autowired
    private ICelesta celesta;

    @PostConstruct
    public void init(){
        MyCursor.onPreInsert(celesta, c ->
                System.out.printf("Record %s is going to be inserted!%n", c.getId()));
    }

    . . .
}

Кодогенерируемый класс MyCursor имеет метод

public static void onPreInsert(ICelesta celesta, Consumer<MyCursor> cursorConsumer)

Системные курсоры также поддерживают возможность регистрировать триггеры.

5.4. Объект xRec

Объект xRec, получаемый с помощью метода getXRec(), предназначен преимущественно для использования в триггере onPreUpdate. Сравнивая поля xRec с текущими значениями полей, можно определить, что именно изменилось в записи.

xRec хранит значения полей, полученные при последнем чтении курсора из базы данных (в отличие от основного буфера, поля которого после чтения равны полям xRec, но затем изменяются, когда пользователь присваивает им новые значения). Обновление объекта xRec происходит только при следующих действиях:

  • [try]first(),

  • [try]get(),

  • next(),

  • [try]insert() (по сути после вставки система выполняет операцию get() для курсора, чтобы прочитать значения, выданные базой данных на поля IDENITY, GETDATE(), DEFAULT, обновляя и основной буфер, и xRec),

  • [try]update() (после обновления в БД xRec становится копией текущего курсора),

  • delete() (после обновления в БД xRec заполняется значением буфера, как он был до удаления).

Обратите внимание, что в pre- и post- триггерах значение xRec будет различным!

5.5. Защита от потерянных обновлений

5.5.1. Что такое потерянное обновление (lost update)?

Чтобы проиллюстрировать понятие "потерянное обновление", рассмотрим следующий сценарий.

Допустим, в приложении имеется таблица с перечнем клиентов и пользователи могут редактировать эту таблицу через формы-карточки. Пусть события развиваются в следующей последовательности:

  1. Пользователь А открывает карточку клиента для того, чтобы отредактировать его почтовый индекс.

  2. Пользователь Б независимо от пользователя А на своей машине открывает карточку клиента для того, чтобы отредактировать значение его кредитного лимита.

  3. Пользователь Б, изменив значение поля "кредитный лимит", сохраняет карточку. В базу данных записалась информация от пользователя Б, но пользователь А продолжает работу со своей копией карточки, где значение поля "кредитный лимит" ещё не обновилось.

  4. Пользователь А заканчивает редактировать почтовый индекс клиента и сохраняет свою копию карточки. В базу данных сохраняются все поля карточки, в том числе старый кредитный лимит.

В итоге получается, что работа пользователя Б потеряна!

Это наиболее расхожий пример, но на самом деле, разработчик решения может столкнуться с потерянными обновлениями и в гораздо более тривиальном случае, не в рамках многопользовательской работы. Предположим, что для модификации одной и той же таблицы используются два курсора:

FooTableCursor rec1 = new FooTableCursor(context);
FooTableCursor rec2 = new FooTableCursor(context);

Пусть в некий момент оба курсора получают данные об одной и той же записи, например, таким образом:

rec1.get(1);
rec2.copyFieldsFrom(rec1);

Допустим, запись таблицы FooTable с id = 1 состоит всего из трёх полей:

id

field1

field2

1

oldvalue

oldvalue

И пусть теперь оба курсора выполняют модификации записи:

rec1.setField1("newvalue");
rec1.update();
//в rec1 и в базе данных уже запись 1 | newvalue | oldvalue
//но в rec2 всё ещё                 1 | oldvalue | oldvalue
rec2.setField2("newvalue");
rec2.update();
//в базе данных теперь будет запись 1 | oldvalue | newvalue ???

Как видим, неудачно написанный код даже в рамках однопользовательской работы может столкнуться с явлением потери обновлений.

5.5.2. Способы защиты от потерянных обновлений

Исторически для борьбы с явлением потерянного обновления сложились два метода:

  • Метод пессимистической блокировки (pessimistic lock) заключается в том, что при начале редактирования записи из какого-либо места в приложении, запись помечается как заблокированная, и никакой другой скрипт или пользователь не сможет начать редактирование до тех пор, пока предыдущий редактор не завершит свою работу, обновив запись или отказавшись от её редактирования.

  • Метод оптимистической блокировки (optimistic lock) заключается в том, что любому пользователю или скрипту в любое время разрешено начать редактирование записи, при этом в момент извлечения записи из базы данных извлекается и номер версии записи. В момент сохранения записи происходит проверка: если номер сохраняемой версии совпадает с таковым в базе данных, запись сохраняется и номер версии записи в базе данных инкрементируется; если же номер сохраняемой версии меньше, чем номер версии записи в базе данных, то пользователю выдаётся ошибка с сообщением о том, что кто-то поменял запись прежде и совет прочитать запись заново.

Каждый из методов, разумеется, имеет свои недостатки.

Главным недостатком оптимистической блокировки является, конечно, то, что пользователю не удастся записать результат своей работы в базу данных, если кто-то успел выполнить обновление той же самой записи прежде. Тем не менее, на практике это происходит в довольно редких случаях, и "страдают" от этого в основном лишь самые "нерасторопные" пользователи, у которых слишком большое время проходит от извлечения записи до завершения её редактирования.

Главным недостатком пессимистической блокировки является то, что от пользователя ожидается, что, начав редактирование записи, он явным образом завершит редактирование или откажется от него, сняв с записи блокировку. Однако на практике, если блокировка записи продолжается слишком долго, невозможно понять, какой из случаев имеет место:

  1. пользователь продолжает активную и сложную работу с записью и нужно ждать завершения его работы?

  2. пользователю уже не нужно редактировать запись, но он просто забыл нажать на кнопку "отмена" и ушёл обедать/отправился в отпуск на две недели/уволился из организации?

  3. у пользователя разорвалась связь с сервером/завис клиент/перезагрузился компьютер?

Во втором и третьем случаях требуется внешнее вмешательство и явное снятие блокировки силами администраторов, иначе другие пользователи не смогут работать с заблокированной записью. Случай 2 никогда нельзя исключать в организации, где работают живые люди, а случай 3 особенно вероятен в условиях клиент-серверной работы в Web-среде.

В целом для систем, подобных Celesta, недостатки пессимистической блокировки являются гораздо более существенными, чем недостатки оптимистической блокировки, и поэтому Celesta использует метод оптимистической блокировки для борьбы с потерянными обновлениями.

5.5.3. Защита от потерянных обновлений в Celesta

По умолчанию, всякая таблица в системе Celesta снабжается системным полем recversion с типом INT NOT NULL.

Данное поле создаётся автоматически, разработчику не следует включать это поле в CREATE TABLE-скрипт. Более того, разработчик не может создать собственное поле с именем recversion. Доступ к этому полю имеется через классы доступа к данным, как к обычному полю.

При вставке новой записи поле recversion принимает значение по умолчанию 1 (единица).

При обновлении записи специальный триггер базы данных проверяет тот факт, что новое значение этого поля совпадает со значением, существующим в базе данных: если совпадение установлено, поле инкрементируется, если нет — генерируется ошибка:

 Can not update <имя гранулы и таблицы> ([<значения полей первичного ключа>]): this record has been already modified by someone. Please start updating again.

В двух рассмотренных выше примерах Celesta выдаст ошибку и не даст отправить в базу данных запись, приводящую к потерянным обновлениям.

Иногда возникает необходимость отказаться от защиты от потерянных обновлений — например, если нет желания поддерживать системное поле recversion и специальные триггеры. В этом случае при создании таблицы на языке CelestaSQL необходимо использовать опцию WITH NO VERSION CHECK после определения таблицы.

5.6. CelestaUnit

Обычно автоматическое тестирование систем, редактирующих данные, представляет определённые сложности и требует использования специальных приёмов (например, развёртывание базы данных в контейнере). Такие тесты обычно выполняются не быстро и разработчики избегают их.

В Celesta тестирование методов, редактирующих данные, осуществляется на уровне очень быстро выполняющихся модульных тестов, для чего разработано расширение JUnit5. Модульные тесты выполняются на встроенной непосредственно в Celesta базе H2, работающей в режиме in-memory. Эта база не требует установки, запускается моментально и исчезает после завершения тестов.

Чтобы воспользоваться данной функциональностью, необходимо добавить модуль celesta-unit Maven-зависимости проекта:

<dependency>
    <groupId>ru.curs</groupId>
    <artifactId>celesta-unit<artifactId>
    <version>...</version>
    <scope>test</scope>
</dependency>

Также в тестовый scope проекта необходимо добавить зависимости JUnit5 (примеры см. в документации Junit5).

5.6.1. Пример пользования

Наиболее простым способом использования является добавление аннотации @CelestaTest к тестовому классу и использование параметров с типом CallContext в тестах:

/*Аннотация CelestaTest подключает JUnit5 extension class ru.curs.celestaunit.CelestaUnitExtension,
обеспечивающий подстановку CallContext-объектов в параметры тестов.*/
@CelestaTest
public class DocumentServiceTest {

    /*Сервис может быть создан как напрямую,
    так и используя DI контейнеры */

    DocumentService srv = new DocumentService();

    @Test
    /*Параметр CallContext будет подставлен автоматически,
    на основе временной базы данных H2*/
    void documentIsPutToDb(CallContext context) throws ParseException {
        /*Вызываем сервис*/
        srv.postOrder(context, ...);
        /*Проверяем, что данные попали в базу*/
        OrderHeaderCursor header = new OrderHeaderCursor(context);
        header.tryFirst();
        assertEquals("no1", header.getId());
    }
}

Таким образом, каждый из тестов может получать в качестве параметра активный CallContext. Этот контекст формируется на основе базы данных H2, в которой развёрнута Celesta score, и может быть использован для создания курсоров.

5.6.2. Изменение настроек CelestaUnit по умолчанию

CelestaUnit работает со следующими умолчаниями:

  • Score path: src/main/resources/score.

  • Проверка ссылочной целостности (по Foreign keys) по умолчанию включена.

  • Очистка таблиц после каждого теста по умолчанию включена.

Изменить умолчания можно, воспользовавшись в тестовом классе программной регистрацией расширения CelestaUnitExtension в JUnit5:

public class DocumentServiceTest {
    public static final String SCORE_PATH = "src/test/resources/score";
    @RegisterExtension
    static CelestaUnitExtension ext =
            CelestaUnitExtension.builder()
                    .withScorePath(SCORE_PATH)
                    .withReferentialIntegrity(true)
                    .withTruncateAfterEach(false).build();

Например, в ряде случаев бывает полезно отключить проверку ссылочной целостности, что упрощает добавление тестовых данных в таблицы, связанные внешними ключами с другими таблицами.

6. Часть 5. Работа с метаданными Celesta

6.1. Метаданные Celesta

6.1.1. Метаданные и их динамическое изменение

Экземпляр класса Celesta доступен через метод getCelesta() переменной CallContext context, передаваемой в качестве аргумента в каждую процедуру обработки данных.

Через метод getScore() экземпляра класса Celesta разработчик решения может получить доступ к метаданным системы, построенным при разборе «партитуры» (парсинге SQL-файлов). Доступ к метаданным необходим для двух целей:

  1. получения информации о текущей структуре базы данных во время выполнения кода бизнес-логики,

  2. динамического изменения структуры базы данных.

Для динамического изменения структуры базы данных необходимо действовать в три этапа:

  1. Используя описанные далее методы объектов метаданных, изменить объектную модель данных в памяти (ограничение: попытка изменения объектов системной гранулы "celesta" приведёт к ошибке).

  2. Вызвать метод save() объекта Score. При этом система сериализует текущее состояние метаданных гранул в .sql-файлы партитуры Celesta, перезаписывая их текущее содержимое. Перезаписываться будут лишь файлы тех гранул, которые были изменены.

  3. Создать новый экземпляр класса Celesta. При этом произойдёт инициализация объекта Celesta на основе новой структуры базы данных и автоматическая миграция базы данных.

Следует понимать, что хотя Celesta не будет перезаписывать .sql-файл, если в этом нет нужды (если метаданные не были изменены), при перезаписи содержимого .sql-файлов происходит необратимая потеря комментариев и форматирования, существовавших в них ранее. Сохранены в тексте будут только CelestaDoc-комментарии. Поэтому динамическим изменением следует пользоваться с осторожностью.

6.1.2. Состав метаданных

Все метаданные (Score) делятся на гранулы (Grain), состоящие из таблиц (Table), индексов (Index) и представлений (View).

  • Таблицы состоят из столбцов (Column) и содержат внешние ключи.

  • Индексы относятся к таблицам и состоят из их столбцов (Column).

  • Представления состоят из столбцов представлений (ViewColumnMeta), которые отличаются от столбцов таблиц, но имеют ряд общих свойств.

Ниже представлена диаграмма классов, описывающих метаданные.

metaclasses

Базовым интерфейсом для столбцов таблиц и представлений является интерфейс ColumnMeta, с помощью которого можно узнать Celesta-тип данных столбца, его nullability и CelestaDoc, привязанный к данному столбцу. Данный интерфейс реализуют классы ViewColumnMeta для описания полей представлений и Column для описания полей таблиц.

Класс Column является абстрактным, и для шести типов полей, поддерживаемых Celesta, от него наследуются шесть субклассов:

columnclasses

6.1.3. Методы модификации метаданных

Модификация метаданных во время выполнения возможна следующим образом:

Тип объекта

Методы добавления

Методы модификации

Метод удаления

Гранула

Grain(score, name) — конструктор создаёт объект-гранулу с именем name, привязанную к score.

setVersion(version) — устанавливает тэг версии (version tag).

метод отсутствует, гранула может быть удалена только физическим удалением папки с гранулой из Score.

Таблица

Table(grain, name) — конструктор создаёт объект-таблицу с именем name, привязанную к объекту-грануле grain.

setPK(columnName, …​) — устанавливает первичный ключ таблицы на основе переданного массива имён столбцов. Попытка установки пустого первичного ключа приводит к ошибке.

delete() — удаляет таблицу из гранулы

Поле

BinaryColumn(table, name) BooleanColumn(table, name) DateTimeColumn(table, name) ZonedDateTimeColumn(table, name) FloatingColumn(table, name) DecimalColumn(table, name, precision, scale) IntegerColumn(table, name) StringColumn(table, name) — создаёт поле нужного типа с именем name, привязанное к объекту-таблице table.

setNullableAndDefault(nullable, defaultValue) — первый (булевский) аргумент этого метода управляет свойством isNull, второй (строковый) — задаёт значение DEFAULT, используя синтаксис соответствующего определения поля. Например, для поля даты здесь допустимо использование слова GETDATE. setLength(length) — определён только для StringColumn, устанавливает длину строки. Возможно использование слова MAX в качестве длины. setPrecision(precision) — определён только для DecimalColumn, устанавливает длину числа в символах. setScale(scale) — определён только для DecimalColumn, устанавливает длину дробной части в символах.

delete() — удаляет поле из таблицы

Внешний ключ

ForeignKey(parentTable, referencedTable, columnNames) — конструктор создаёт объект-внешний ключ, привязанный к таблице parentTable, ссылающийся на referencedTable. В массиве строк columnNames передаётся список имён столбцов, с которых идёт ссылка.

setUpdateRule(updateRule) setDeleteRule(deleteRule) — устанавливает поведение внешнего ключа на обновление или удаление записи, на которую есть ссылка (SET NULL, CASCADE, NO_ACTION). В качестве аргумента следует передавать одно из значений перечисления FKRule.

delete() — удаляет внешний ключ

Индекс

Index(table, name, columnNames) — создаёт индекс на таблице table с именем name и списком полей columnNames, передаваемом как массив строк.

delete() — удаляет индекс

Представление

View(grain, name, sql) — создаёт представление в грануле grain, с именем name, на основе sql-запроса sql.

Для того, чтобы изменить sql-запрос, на основе которого сделано представление, удалите существующее представление и создайте новое с тем же именем. getCelestaQueryString() — возвращает SQL-запрос представления getColumns() — возвращает перечень имён столбцов представления

delete() — удаляет представление

У каждого класса-наследника NamedElement имеются также методы getCelestaDoc() и setCelestaDoc() для чтения и установки документирующих данных CelestaDoc. При сохранении в файл динамически изменённых метаданных CelestaDoc-комментарии сохраняются.

7. Приложение

7.1. Особенности работы Celesta с поддерживаемыми типами СУБД

Система по возможности прозрачно для разработчиков решения поддерживает MS SQL Server, Oracle, Postgre SQL и H2.

Хотя решения Celesta свободно переносимы между разными типами поддерживаемых СУБД, тем не менее, каждая из этих СУБД имеет особенности настройки. Кроме того, разные функциональные возможности Celesta по-разному реализованы в разных СУБД. Этим особенностям посвящён данный раздел.

Соответствие типов данных Celesta и СУБД приведено в разделе «Язык Celesta-SQL: типы данных».

Mssql

7.1.1. MS SQL Server

Особенности реализации

Понятию «гранула» соответствует понятие «схема» (SCHEMA).

Ora

7.1.2. Oracle

Особенности настройки

Ошибка ORA-12705: Cannot access NLS data files…​ Если при запуске Celesta на Oracle Express Edition возникает ошибка "ORA-12705: Cannot access NLS data files or invalid environment specified", в числе аргументов JVM необходимо задать параметр

-Duser.country=US

Если Celesta запускается из Flute или Showcase, то задать этот параметр можно на вкладке Java, поле Java Options программ Flute2w.exe или Tomcat7w.exe, используемых для управления сервисами Flute/Tomcat.

Эта проблема является общей для связки Oracle XE + JDBC и актуальна только для Oracle Express Edition, в прочих (production) версиях Oracle Database она не актуальна.

Минимальные настройки прав доступа для USER’а в БД Oracle 11g:

GRANT
        CONNECT,
        RESOURCE,
        CREATE TABLE,
        CREATE PROCEDURE,
        CREATE VIEW,
        CREATE SEQUENCE,
        CREATE TRIGGER,
        SELECT ANY DICTIONARY
        TO <USER>

В некоторых организациях по умолчанию не дают право доступа SELECT ANY DICTIONARY, из-за чего может возникать ошибка "ORA-00942: table or view does not exist" при разворачивании системной гранулы Celesta.

Особенности реализации
  • Гранула — префикс в имени таблицы, отделённый знаком подчёркивания, при этом работает ограничение Oracle на длину имени таблицы — 30 символов. Причина в том, что понятие «схема» в Oracle несколько отличается от такового для других СУБД, создание «схем» в Oracle связано с созданием новых пользователей, на что на практике не могут быть выданы права администраторами Oracle-серверов, на которых хранятся промышленные данные.

  • Oracle не поддерживает конструкцию FOREIGN KEY …​ ON UPDATE/DELETE SET NULL, поэтому она эмулируется при помощи триггеров.

Postgre

7.1.3. PostgreSQL

Особенности реализации
  • Понятию «гранула» соответствует понятие «схема» (SCHEMA).

Особенности использования

При использовании Celesta для доступа к существующей заранее (а не создаваемой и обновляемой в Celesta) базе данных может возникнуть проблема с полями типа uuid. Сама Celesta типа данных uuid как такового не поддерживает, но может работать с ним через поле типа VARCHAR(36). При этом не возникает проблем в MS SQL Server, но для работы Celesta в Postgres требуется явно определить оператор сравнения varchar с uuid и имплицитное изменение типов при присвоении:

CREATE OR REPLACE FUNCTION celesta.uuidequal(a1 uuid, a2 CHARACTER VARYING)
  RETURNS BOOLEAN AS 'select a1::varchar = a2'
  LANGUAGE SQL IMMUTABLE;
CREATE OPERATOR = (
    LEFTARG = uuid,
    RIGHTARG = CHARACTER VARYING,
    PROCEDURE = celesta.uuidequal
);
CREATE CAST (character varying AS uuid)
    WITH  INOUT AS ASSIGNMENT;
H2

7.1.4. H2

Особенности настройки
  • Для упрощенной инициализации inmemory db в файл celesta.properties можно добавить настройку "h2.in-memory=true". В таком случае строка jdbc подключения будет игнорироваться, а также логин и пароль пользователя.

  • В celesta.properties добавлена поддержка настройки h2.referential.integrity=false(по умолчанию)/true. Выключенная настройка означает, что ограничения типа constraint будут проигнорированы при записи в БД. При включении ограничения будут обрабатываться как в других РСУБД. Для установки данной настройки не в inmemory БД пользователь должен обладать правами администратора(поле "ADMIN" в таблице "INFORMATION_SCHEMA.USERS") Данная настройка срабатывает один раз при инициализации приложения и для обновления требуется его перезапуск.

Особенности реализации
  • Понятию «гранула» соответствует понятие «схема» (SCHEMA).

  • Поля 'recversion' управляются триггером, написанном на Java и реализующим интерфейс org.h2.api.Trigger. H2 не поддерживает триггеры, логика которых заключена в процедурном SQL.

7.2. Проектирование базы данных Celesta в DBSchema

7.2.1. Синхронизация метаданных Celesta и проекта DBSchema

DBSchema представляет собой удобный инструмент визуального моделирования структуры базы данных. Имеется возможность полностью моделировать в DBSchema всю информацию Celesta о структуре БД (таблицы, поля, ключи, индексы), а также наоборот — превратить существующий Score в проект DBSchema.

800px Dbschemascreen
Настройка системы

Для работы вам понадобится:

  1. DBSchema (программа проприетарная, для работы необходимо приобретать лицензию).

  2. Утилита dbschemasync.

  3. Папка с настройками Celesta для DBSchema, которую надо поместить в %userprofile%\.DbSchema\config\rdbms\. В настройках прописаны шесть типов данных, доступных в Celesta.

Актуальную версию утилиты dbschemasync и папки с настройками для DBSchema (dbschemasync.zip) следует брать с Artifactory: https://artifactory.corchestra.ru/artifactory/list/corchestra-dev/

Запуск синхронизации

Утилита dbschemasync принимает два параметра:

  • Score path

  • имя DBS-файла (проекта DBSchema)

Направление синхронизации определяется последовательностью аргументов: если первым аргументом идёт Score Path, то синхронизация идёт от Score к проекту DBSchema, если же первым аргументом идёт имя проекта DBSchema, то синхронизация идёт от DBSchema к Score.

Пример команды для синхронизации от score к схеме:

dbschemasync "c:/temp/dbschema/score/" "c:/temp/dbschema/schema.dbs"

Пример синхронизации от схемы к score:

dbschemasync "c:/temp/dbschema/schema.dbs" "c:/temp/dbschema/score/"
Особенности работы при дизайне структуры БД

Всё, что находится в CelestaDoc, переводится в Documentation-поля DBSchema, и наоборот.

800px Dbschemascreen3

Опции таблицы (WITH (NO) VERSION CHECK, WITH READ ONLY…​) находятся на вкладке Storage:

Dbschemascreen4

В DBSchema, чтобы задать версию гранулы, необходимо модифицировать мнимую "хранимую процедуру", имеющую то же название, что и гранула (сами хранимые процедуры для Celesta в DBSchema, естественно, не моделируются):

Dbschemascreen2

7.2.2. Создание CelestaSQL-скриптов на основе структуры существующей БД

Эта технология рекомендована всем, у кого возникнет задача создания CelestaSQL-скриптов для уже существующей базы данных. Использование каких-либо иных путей (например, выгрузка SQL-скрипта из базы данных и ручная его "вычистка") по опыту является гораздо более трудоёмким занятием!

Для этого требуется программа DBSchema с установленной поддержкой Celesta. Шаги следующие:

  1. Присоединяем DBSchema к нужной базе данных и методом Reverse Engineering закачиваем всю нужную нам структуру (на этом этапе можно ограничиться только теми таблицами и связями, которые нас интересуют в celesta). Убедитесь, что "забрали" все нужные таблицы, т. к. на следующих шагах проект придётся "отсоединить" от базы данных и автоматически получить сведения о таблицах уже не получится.

  2. Отключаем DBSchema от базы данных (offline) и меняем тип базы с SQL Server на Celesta через меню Project-→Project Settings (естественно, предполагается, что к этому моменту в DBSchema установлена надстройка для синхронизации с Celesta из файла dbschemasetup.zip). Откроется окно, в котором DBSchema предложит сопоставить типы. Здесь надо всё правильно сделать: например, для SQL Server decimal(10,3) перевести в REAL, varchar(max) — в TEXT, uuid в varchar(36) и так далее.

  3. После нажатия на OK мы получаем DBSchema-проект, ориентированный на Челесту (в этот момент его уместно сохранить в отдельном месте: он больше не привязан к исходной базе данных). Однако это — некорректный проект, т. к. нет деления на гранулы, гранулам не даны объявления, и, что главное — в нём ещё много фич, нехарактерных для Челесты. На этом этапе мы вручную должны создать гранулы (схемы) и разложить по ним таблицы.

  4. Если теперь воспользоваться утилитой schemasync.jar, то мы, скорее всего, получим сообщения об ошибках, т. к. проект остаётся некорректным. Поэтому следует воспользоваться командой Schema-→Generate Schema Script для выгрузки всех таблиц в один sql-файл. Получившийся скрипт будет очень хорошим приближением к Celesta-скрипту, и вот это приближение уже удобно доделать вручную: убрать использование не поддерживаемых в Celesta функций, раздробить на разные файлы, что-то сделать с названиями длиннее 30 символов и т. д.

  5. Настало время пробовать запускать Celesta с вашими скриптами.

  6. Если вы дальше желаете использовать DBSchema, то лишь после того, как Celesta "согласится" работать с вашими скриптами, имеет смысл воспользоваться утилитой schemasync.jar для связи с абсолютно пустым проектом DBSchema ("промежуточный" проект DBSchema вы можете удалить). Это связано с тем, что в schemasync.jar встроен "челестовский" парсер SQL-скриптов, и он не сможет работать с тем, с чем не может работать сама Celesta.

7.2.3. Выгрузка в PlantUML

Третья опция командной строки -adoc в режиме конвертации из DBSchema в score параллельно формирует диаграммы в формате PlantUML для каждой из диаграмм DBSchema. Имена файлов диаграмм соответствуют названиям листов DBSchema с диаграммами.