Gymterview
middle

Что такое Active Objects и как с ними работать?

Active Objects (AO) — ORM-фреймворк для плагинов Jira Data Center, позволяющий плагинам хранить данные в БД Jira без создания таблиц вручную. По концепции похож на JPA, но значительно легковеснее.

Аналогия из жизни: Active Objects — это как встроенный шкаф в съёмной квартире. Вы не строите мебель сами (не создаёте таблицы SQL), а описываете, что хотите хранить (интерфейсы), и фреймворк сам организует пространство. При выезде (удалении плагина) шкаф убирается автоматически.

Ключевые особенности

  • Таблицы создаются автоматически из Java-интерфейсов
  • Префикс таблиц — уникальный для каждого плагина (избежание конфликтов)
  • Поддержка миграций (upgrade tasks)
  • Работает с БД Jira (PostgreSQL, MySQL, Oracle, MS SQL)

Определение сущности (entity)

Пример
@Table("TASK_CONFIG")
@Preload  // загружать все поля при выборке (оптимизация)
public interface TaskConfig extends Entity {
    // Entity предоставляет int getID() автоматически

    @StringLength(255)
    @NotNull
    String getProjectKey();
    void setProjectKey(String projectKey);

    @StringLength(StringLength.UNLIMITED)
    String getConfiguration();
    void setConfiguration(String configuration);

    boolean isEnabled();
    void setEnabled(boolean enabled);

    @Default("0")
    int getPriority();
    void setPriority(int priority);

    // Связь один-ко-многим
    @OneToMany(reverse = "getTaskConfig")
    TaskExecution[] getExecutions();
}

@Table("TASK_EXEC")
public interface TaskExecution extends Entity {

    @NotNull
    TaskConfig getTaskConfig();
    void setTaskConfig(TaskConfig config);

    @StringLength(50)
    String getStatus();
    void setStatus(String status);

    long getStartedAt();
    void setStartedAt(long startedAt);

    long getFinishedAt();
    void setFinishedAt(long finishedAt);

    @StringLength(StringLength.UNLIMITED)
    String getErrorMessage();
    void setErrorMessage(String message);
}

Регистрация AO в atlassian-plugin.xml

Пример
<ao key="ao-module">
    <description>Active Objects модуль</description>
    <entity>com.example.plugin.ao.TaskConfig</entity>
    <entity>com.example.plugin.ao.TaskExecution</entity>
</ao>

CRUD-операции

Код TaskConfigService
@Named
public class TaskConfigService {

    private final ActiveObjects ao;

    @Inject
    public TaskConfigService(@ComponentImport ActiveObjects ao) {
        this.ao = ao;
    }

    // CREATE
    public TaskConfig create(String projectKey, String config) {
        return ao.executeInTransaction(() -> {
            TaskConfig entity = ao.create(TaskConfig.class);
            entity.setProjectKey(projectKey);
            entity.setConfiguration(config);
            entity.setEnabled(true);
            entity.setPriority(0);
            entity.save();
            return entity;
        });
    }

    // READ — по ID
    public TaskConfig getById(int id) {
        return ao.get(TaskConfig.class, id);
    }

    // READ — поиск по условию
    public TaskConfig[] findByProject(String projectKey) {
        return ao.find(TaskConfig.class,
                Query.select()
                        .where("PROJECT_KEY = ?", projectKey)
                        .order("PRIORITY DESC")
                        .limit(100));
    }

    // UPDATE
    public void update(int id, String newConfig) {
        ao.executeInTransaction(() -> {
            TaskConfig entity = ao.get(TaskConfig.class, id);
            if (entity != null) {
                entity.setConfiguration(newConfig);
                entity.save();
            }
            return null;
        });
    }

    // DELETE
    public void delete(int id) {
        ao.executeInTransaction(() -> {
            TaskConfig entity = ao.get(TaskConfig.class, id);
            if (entity != null) {
                for (TaskExecution exec : entity.getExecutions()) {
                    ao.delete(exec);
                }
                ao.delete(entity);
            }
            return null;
        });
    }
}

Миграция (Upgrade Task)

Пример
public class UpgradeTask001 implements ActiveObjectsUpgradeTask {

    @Override
    public ModelVersion getModelVersion() {
        return ModelVersion.valueOf("1");
    }

    @Override
    public void upgrade(ModelVersion currentVersion, ActiveObjects ao) {
        ao.migrate(TaskConfig.class);
    }
}
Пример
<ao key="ao-module">
    <entity>com.example.plugin.ao.TaskConfig</entity>
    <entity>com.example.plugin.ao.TaskExecution</entity>
    <upgradeTask>com.example.plugin.upgrade.UpgradeTask001</upgradeTask>
</ao>

Частые ошибки

  • Забыть @Preload — N+1 проблема, каждый доступ к полю = отдельный SELECT
  • Использование Java-имён полей в Query вместо SQL-имён: "projectKey" вместо "PROJECT_KEY"
  • Не обернуть запись в транзакцию — данные могут быть в inconsistent-состоянии
  • Отсутствие upgrade tasks — при первом деплое таблицы не создаются
  • Хранение больших данных без @StringLength(UNLIMITED) — дефолтная длина строки 255 символов

Как используется в 2026

  • Active Objects остаётся стандартом для DC-плагинов
  • Нет альтернатив (JPA/Hibernate недоступны из-за OSGi-изоляции)
  • Для Cloud (Forge) аналог — Forge Storage API и Entity Storage
  • При планировании миграции DC -> Cloud нужно продумать перенос данных из AO в Cloud storage

На собеседовании: подчеркните, что AO — единственный поддерживаемый способ хранения данных плагина в БД Jira DC. Обязательно упомяните @Preload (без него — N+1), UPPERCASE-имена колонок в Query и executeInTransaction() для записи. Это практические знания, которые показывают реальный опыт.