Atlassian Plugins: погружение в Active Objects и Plugin Settings

в 11:26, , рубрики: atlassian, atlassian jira, confluence, Help Desk Software, java, jira, jira plugin, service desk, Блог компании Mail.Ru Group, разработка

Привет! Я работаю в Mail.Ru Group в отделе разработки плагинов JIRA. Плагины позволяют расширять или изменять функциональность приложения. Например, с их помощью можно создавать новые типы полей, гаджеты, JQL-запросы, панели с различной информацией, графики и многое другое.

Большинство наших плагинов требуют хранения дополнительных данных, которые они используют. В этой статье я хочу рассказать, как мы решаем эту задачу. Существует два основных способа хранения таких данных: Active Objects и Plugin Settings. Рассмотрим их поподробнее и разберемся в каком случае лучше и удобнее использовать один, а в каком — другой.

image

1. Active Objects

Active Objects — это библиотека, которая основана на ORM-технологии (Object Relational Mapping). Она связывает базы данных с концепциями объектно-ориентированного программирования, создавая так называемую виртуальную объектную базу данных.

Active Objects применяются для работы с однотипными группами данных. Например, это могут быть списки оборудования, складов, подрядчиков. Такая информация может синхронизироваться с другими сервисами или заноситься вручную. Active Objects подходят также для хранения настроек проектов, полей и многих других.

Создание объектов

Для создания сущности Active Objects используется интерфейс, наследуемый от net.java.ao.Entity:

public interface Product extends Entity {
    String getName();
    void setName(String name);

    double getPrice();
    void setPrice(double price);   
}

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

Для использования Active Objects необходимо подключить библиотеку в pom-файле.

<dependency>
    <groupId>com.atlassian.activeobjects</groupId>
    <artifactId>activeobjects-plugin</artifactId>
    <version>0.23.7</version>
    <scope>provided</scope>
</dependency>

В файл структуры плагина (atlassian-plugin.xml) импортируется компонент ActiveObjects и все созданные сущности.

<component-import key="ao" interface="com.atlassian.activeobjects.external.ActiveObjects" />

<ao key="ao-entities">
    <entity>com.jira.plugins.shop.model.Product</entity>
    <entity>com.jira.plugins.shop.model.Shop</entity>
</ao>

Работа с Active Objects

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

<component key="product-manager" class="com.jira.plugins.shop.ProductManager" />

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

private final ActiveObjects ao;

public Product getProduct(final int id) {
    return ao.executeInTransaction(new TransactionCallback<Shop>() {
        @Override
        public Product doInTransaction() {
            return ao.get(Product.class, id);
        }
    });
}

Часто требуется получать различные выборки данных. С помощью класса net.java.ao.Query можно составлять любые SQL-запросы. Правда, делать это нежелательно, потому что будем завязываться на имена полей из базы данных.

public Product[] getProducts(final String name) {
    return ao.executeInTransaction(new TransactionCallback<Product[]>() {
        @Override
        public Product[] doInTransaction() {
            return ao.find(Product.class, Query.select().where("NAME = ?", name).order("NAME"));
        }
    });
}

Экземпляр Active Objects создается с помощью функции ao.create. Затем, при необходимости, заполняются его поля. Важное замечание: объект нужно сохранить после редактирования, иначе все изменения будут потеряны.

public Product createProduct(final String name, final double price) {
    return ao.executeInTransaction(new TransactionCallback<Product>() {
        @Override
        public Product doInTransaction() {
            Product product = ao.create(Product.class);
            product.setName(name);
            product.setPrice(price);
            product.save();
            return product;
        }
    });
}

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

Удаление происходит при помощи ao.delete, в которую передается сам экземпляр.

public void deleteProduct(final int id) {
    ao.executeInTransaction(new TransactionCallback<Void>() {
        @Override
        public Void doInTransaction() {
            Product product = ao.get(Product.class, id);
            ao.delete(product);
            return null;
        }
    });
}

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

Связи между объектами

Active Objects могут быть связаны друг с другом. Существует три вида связей. Расскажем подробнее о каждой из них.

Связь один к одному

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

public interface Shop extends Entity {
    @OneToOne
    Address getAddress();
}

public interface Address extends Entity {
    Shop getShop();
    void setShop(Shop shop);
}

Для того чтобы связать объекты, нужно обязательно вызвать setShop(Shop shop).

Связь один ко многим

Например, у магазина может быть несколько продавцов, но продавец работает только в одном магазине.

public interface Shop extends Entity {
    @OneToMany
    Seller[] getSellers();
}

public interface Seller extends Entity {
    Shop getShop();
    void setShop(Shop shop);
}

Связь многие ко многим

Например, продукт может быть в разных магазинах сети и в магазине может быть много продуктов. Соответственно, в классах Product и Shop будет применена эта связь. Обязательным является создание третьей сущности, которая будет связывать две другие (как дополнительная таблица в реляционных базах данных).

Аннотация ставится только для get-методов.

public interface Shop extends Entity {
    @ManyToMany(value = ProductToShop.class)
    Product[] getProducts();
}

public interface Product extends Entity {
    @ManyToMany(value = ProductToShop.class)
    Shop[] getShops();
}

public interface ProductToShop extends Entity {
    Product getProduct();
    void setProduct(Product product);

    Shop getShop();
    void setShop(Shop shop);
}

Хранение данных

Active Objects хранятся в отдельной таблице базы данных. По умолчанию название таблицы формируется из трех частей. Первая состоит из приставки AO (Active Objects). Вторая — из шести символов шестнадцатеричного значения MD5 хеш-функции ключа плагина или, если присутствует, атрибута namespace модуля Active Objects. Последняя часть представляет собой название сущности Active Objects. Пример стандартного названия выглядит так: AO_28BE2D_MY_OBJECT.

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

Active Objects работают со следующими типами данных:

  • текст (TEXT, VARCHAR);
  • числа (INTEGER, BIGINT, DOUBLE);
  • дата и время (DATETIME);
  • логический тип (BOOLEAN).

Переименование таблиц и столбцов

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

Чтобы изменить стандартное название таблицы, необходимо использовать аннотацию @Table("NewName").

@Table("Item")
public interface Product extends Entity {
    double getPrice();
    void getPrice(double price);
}

При переименовании полей требуется применить аннотации @Mutator("NewName") и @Accessor("NewName"). При этом названия столбцов в самой таблице не будут изменены. Аннотация @Accessor указывается для функции, которая возвращает значение, а @Mutator — для функции, которая его изменяет.

public interface Product extends Entity {
    @Accessor("Cost")
    double getPrice();
    @Mutator("Cost")
    void getPrice(double price);
}

Подводные камни

На данный момент Active Objects не работают с типом данных BLOB. В таком случае информацию можно хранить в файловой системе напрямую.

Также существует проблема при работе со связями. Объясним ее на примере. У нас есть две сущности: адрес и магазин. Между ними установлена связь один к одному. Пусть было изменено название города. Если запросить из адреса объект магазина, а из него объект адреса, то значение города возвратится в старом варианте. Дело в том, что поля при изменении объекта не инициализируются заново. В таком случае, если у объекта есть ссылки на другие объекты, необходимо после его изменения снова его проинициализировать.

В операциях создания и поиска в базе данных может иметь значение регистр букв в имени столбцов. Также длина не может превышать 30 символов и нельзя использовать зарезервированные слова: BLOB, CLOB, NUMBER, ROWID, TIMESTAMP, VARCHAR2.

Если в объекте требуется длинное текстовое поле, то перед ним ставится аннотация @StringLength(StringLength.UNLIMITED). Так как, например, в MySQL обычный String будет иметь длину 255 символов.

2. Plugin Settings

Plugin Settings являются частью Shared Access Layer в фреймворке Atlassian. Они обеспечивают хранение данных в виде пары ключ-значение, на которые будет ссылаться плагин во время работы.

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

Создание настроек и их использование

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

public interface PluginData {
    String getDistributingFacilitiesName();
    void setDistributingFacilitiesName(String distributingFacilitiesName);
}

public class PluginDataImpl implements PluginData {
    private static final String PLUGIN_PREFIX = "com.jira.plugins.shop:";
    private static final String DISTRIBUTING_FACILITIES_NAME = PLUGIN_PREFIX + "distributingFacilitiesName";
    
    private final PluginSettingsFactory pluginSettingsFactory;

    public PluginDataImpl(PluginSettingsFactory pluginSettingsFactory) {
        this.pluginSettingsFactory = pluginSettingsFactory;
    }

    @Override
    public String getDistributingFacilities() {
        return (String) pluginSettingsFactory.createGlobalSettings().get(DISTRIBUTING_FACILITIES_NAME);
    }

    @Override
    public void getDistributingFacilities(String distributingFacilitiesName) {
        pluginSettingsFactory.createGlobalSettings().put(DISTRIBUTING_FACILITIES_NAME, distributingFacilitiesName);
    }
}

Можно создать как глобальные настройки, так и локальные по проекту:

  • pluginSettingsFactory.createGlobalSettings() — глобальные;
  • pluginSettingsFactory.createSettingsForKey(projectKey) — локальные, где projectKey — ключ проекта.

Хранение данных

Информация хранится в таблицах в базе данных. В таблице propertyentry записываются имя, ключ и тип значения Plugin Settings. Значение свойства записывается в таблицу, соответствующую его типу, например propertystring. Типы данных, поддерживаемые Plugin Settings:

  • текст (TEXT, LONGTEXT);
  • числа (DECIMAL(18,6), DECIMAL(18,0));
  • дата и время (DATETIME);
  • большие данные (BLOB).

Подводные камни

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

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

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

Заключение

Мы рассмотрели два способа хранения данных в плагинах. Для единичных конфигураций подходят Plugin Settings. Их структура в виде ключ-значение позволяет быстро получать необходимую настройку. С помощью Plugin Settings можно создавать как локальные, так и глобальные конфигурации.

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

Используемые источники:

  1. Документация по Active Objects от Atlassian
  2. Введение в плагин Active Objects
  3. Документация по Plugin Settings от Atlassian
  4. Как и где храняться Plugin Settings

P. S. У нас есть профессиональное сообщество в социальных сетях, где мы обсуждаем использование продуктов Atlassian, обмениваемся опытом и даже устраиваем живые митапы. Пишите свои плагины и делитесь результатами! Присоединяйтесь:

Автор: Mail.Ru Group

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js