Схемы базы данных
Как аддон объявляет свои таблицы в базе данных Identica и обновляет их между версиями.
Аддон, который хранит данные в базе данных Identica, объявляет свои таблицы как DatabaseSchema и устанавливает их через SchemaInstaller — оба интерфейса входят в API базы данных Identica. На новой установке Identica создаёт таблицы, на существующей — выполняет миграции аддона, а в таблице strata_history записывает, до какой версии дошла каждая схема. Прокси с общей базой данных устанавливают схему один раз: первый берёт блокировку, а остальные ждут его.
У каждого владельца таблиц своя схема, которая называется потоком:
| Поток | Владелец | Устанавливается |
|---|---|---|
identica/account | Identica: аккаунты, привязки и профили провайдеров, резервирования, ники | когда Identica открывает базу данных |
identica/feature/verification | функция верификации | когда функция включена |
identica/provider/credential | провайдер Credential | когда провайдер загружен |
identica/integration/statistics | статистика использования: аренда, по которой выбирается прокси, отправляющий общие счётчики | когда запускается статистика использования, после загрузки провайдеров |
Зависимости
Контракты базы данных находятся в me.whereareiam.identica:database-api, рядом с me.whereareiam.identica:api. Таблицы объявляются аннотациями Dialectica, строки читаются и записываются через EntityAccess из Dialectica, а миграции — это миграции Strata. Identica предоставляет все эти библиотеки во время работы, поэтому подключайте их только для компиляции:
dependencies {
compileOnly("me.whereareiam.identica:api:<version>")
compileOnly("me.whereareiam.identica:database-api:<version>")
compileOnly("me.whereareiam:dialectica:4.0.0")
compileOnly("me.whereareiam:dialectica-adapter-jdbi:4.0.0")
compileOnly("me.whereareiam:strata-adapter-database:3.1.1")
compileOnly("me.whereareiam:strata-adapter-database-dialectica:3.1.1")
}Identica загружает Dialectica и Strata под их собственными именами пакетов и делит их со своими аддонами, поэтому обычный jar-файл аддона использует их как есть: он не включает их в себя и не перемещает (relocate). Собственный Jdbi аддону не нужен — Jdbi Identica остаётся внутри плагина.
Объявление таблиц
Каждая таблица — это класс сущности с аннотациями Dialectica. Таблица, которая принадлежит аккаунту, ссылается на таблицу аккаунтов Identica по имени через AccountTable, чтобы её строки удалялись вместе с аккаунтом:
import me.whereareiam.dialectica.entity.annotation.Id;
import me.whereareiam.dialectica.entity.annotation.Index;
import me.whereareiam.dialectica.entity.annotation.Length;
import me.whereareiam.dialectica.entity.annotation.Nullable;
import me.whereareiam.dialectica.entity.annotation.References;
import me.whereareiam.dialectica.entity.annotation.Table;
import me.whereareiam.dialectica.schema.type.ReferentialAction;
import me.whereareiam.identica.database.schema.AccountTable;
@Table("example_notes")
@Index(name = "example_notes_created_at", columns = "createdAt")
public record NoteEntity(
@Id @Length(64) String noteId,
@References(table = AccountTable.NAME, column = AccountTable.KEY,
onDelete = ReferentialAction.CASCADE) UUID uniqueId,
String text,
@Nullable Long createdAt
) {}Добавляйте к именам таблиц и индексов префикс с именем аддона. В README Dialectica перечислены все аннотации и тип столбца, который каждый тип Java получает в каждой базе данных.
Объявление схемы
Схема называет свой поток, перечисляет свои таблицы в их новейшем виде и перечисляет потоки, на таблицы которых она ссылается:
import me.whereareiam.dialectica.Dialectica;
import me.whereareiam.dialectica.schema.Schema;
import me.whereareiam.identica.database.schema.DatabaseSchema;
public final class NotesSchema implements DatabaseSchema {
@Override
public @NotNull String stream() {
return "example/notes";
}
@Override
public @NotNull Schema schema() {
return Schema.of(Dialectica.entity(NoteEntity.class).table());
}
@Override
public @NotNull Set<String> requires() {
return Set.of(ACCOUNT_STREAM);
}
}Имя потока состоит из сегментов в нижнем регистре, разделённых косой чертой, и после релиза никогда не меняется.
Установка схемы
Устанавливайте схему при загрузке аддона, до того как он начнёт читать или записывать свои таблицы:
identica.findService(SchemaInstaller.class).orElseThrow().install(new NotesSchema());Провайдер вместо этого привязывает свои схемы в собственном модуле, и Identica устанавливает их при загрузке провайдера:
Multibinder.newSetBinder(binder(), DatabaseSchema.class).addBinding().to(NotesSchema.class);Установка отклоняет схему, если потоки, от которых она зависит, ещё не установлены, а повторная установка той же схемы ничего не меняет. Схема, которую не удаётся привести к актуальному состоянию, выбрасывает MigrationException из Strata; в этом случае аддон не должен продолжать работу со своими таблицами.
Чтение и запись строк
DatabaseService выдаёт для сущности EntityAccess из Dialectica. Он работает на соединении Identica и говорит на диалекте её базы данных, поэтому столбцы хранятся так, как их объявляет схема:
DatabaseService database = identica.findService(DatabaseService.class).orElseThrow();
EntityAccess<NoteEntity> notes = database.access(NoteEntity.class);
notes.upsert(new NoteEntity("first", uniqueId, "Remember the milk", null));
Optional<NoteEntity> note = notes.find("first");EntityAccess вставляет строки (insert, upsert), изменяет (update), находит (find) и удаляет (delete) их по первичному ключу. Каждая операция описана в README Dialectica.
Последующие изменения таблиц
Схема, вышедшая в релиз, меняется через миграции. Добавляйте каждое изменение как следующую по номеру миграцию и меняйте сущность так, чтобы schema() по-прежнему описывал новейший вид — именно его получает новая установка:
@Override
public @NotNull List<Migration<DatabaseContext>> migrations() {
return List.of(
SchemaMigration.of(1, "note-title",
new AddColumn("example_notes", ColumnDefinition.builder("title", ColumnType.varchar(64)).build())),
new Migration<>(2, "fill-titles", database -> database.update(
"UPDATE example_notes SET title = 'Untitled' WHERE title IS NULL"))
);
}- Версии начинаются с 1, идут без пропусков и после релиза никогда не перенумеровываются и не меняются.
SchemaMigrationприменяет изменения схемы Dialectica, которые пропускают то, что в базе данных уже есть; изменение данных — это обычнаяMigration.- Миграция записывает только в собственные таблицы аддона.
Strata по порядку выполняет миграции, которых установка ещё не видела, и записывает каждую из них. Когда поток впервые выполняется на базе данных, в которой его таблицы уже есть — например, потому что аддон создал их до того, как начал использовать миграции, — его метод baseline сообщает, в каком состоянии находится эта база данных. По умолчанию каждая такая база данных считается новой установкой; переопределите метод, чтобы он возвращал версию, которой соответствуют существующие таблицы:
@Override
public @NotNull OptionalInt baseline(@NotNull DatabaseContext database, int latest) throws SQLException {
return database.tableExists("example_notes") ? OptionalInt.of(0) : OptionalInt.empty();
}OptionalInt.empty() означает новую установку, которая получает schema(); 0 запускает все миграции; любая другая версия запускает миграции, идущие после неё.
Где уместен SQL
- Структура таблиц, их столбцов и индексов всегда объявляется через Dialectica — при создании, изменении и удалении: сущности описывают текущий вид, а миграция меняет структуру изменениями схемы Dialectica.
- Во время работы аддон читает и записывает свои строки через
EntityAccessиз Dialectica. Написанные вручную запросы не входят в API для аддонов: Jdbi Identica остаётся внутри плагина, где собственные модули Identica используют его для того, чегоEntityAccessне умеет. - Разовый шаг с данными внутри миграции, который переносит или преобразует строки прежней структуры, может быть обычным SQL. Миграция описывает один неизменный шаг от точно известной прежней структуры и потом никогда не меняется, поэтому её SQL не следует за сущностями.
Такой шаг с данными:
- использует стандартный SQL, который без изменений выполняется в каждой базе данных, поддерживаемой Identica;
- безопасно выполняется повторно, например как
INSERT … SELECT … WHERE NOT EXISTS, чтобы миграция, остановившаяся на полпути, завершилась при следующем запуске; - покрыт тестами, которые обновляют прежнюю структуру в каждой поддерживаемой базе данных, выполняют миграцию ещё раз и сравнивают результат с новой установкой.
Так устроена миграция username-layout в AccountSchema самой Identica: Dialectica создаёт таблицы ников, два запроса INSERT … SELECT … WHERE NOT EXISTS переносят в них строки Identica 2.2.1, а Dialectica удаляет старую таблицу и столбец.
Значение, которое SQL вычисляет по-разному в разных базах данных, вычисляется в Java: шаг с данными читает строки, связывает и читает каждое значение через Dialect своего соединения (Dialect.of(connection.getMetaData())) и записывает результат. Так устроена миграция username-table в AccountSchema, потому что LOWER в SQLite меняет только буквы ASCII: Dialectica создаёт таблицу identica_account_usernames с уникальным индексом по username_key, шаг записывает туда ник каждого аккаунта с его источником и ключом в нижнем регистре, который получает только самый старый аккаунт с этим ником, а Dialectica удаляет старый столбец username и таблицу состояния ников. Шаг записывает все ники заново, пока старый столбец существует, поэтому он тоже завершается при следующем запуске, если прервался на полпути.
