Identica
Расширение

Схемы базы данных

Как аддон объявляет свои таблицы в базе данных Identica и обновляет их между версиями.

Аддон, который хранит данные в базе данных Identica, объявляет свои таблицы как DatabaseSchema и устанавливает их через SchemaInstaller — оба интерфейса входят в API базы данных Identica. На новой установке Identica создаёт таблицы, на существующей — выполняет миграции аддона, а в таблице strata_history записывает, до какой версии дошла каждая схема. Прокси с общей базой данных устанавливают схему один раз: первый берёт блокировку, а остальные ждут его.

У каждого владельца таблиц своя схема, которая называется потоком:

ПотокВладелецУстанавливается
identica/accountIdentica: аккаунты, привязки и профили провайдеров, резервирования, никикогда 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 и таблицу состояния ников. Шаг записывает все ники заново, пока старый столбец существует, поэтому он тоже завершается при следующем запуске, если прервался на полпути.

На этой странице