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

от автора

Привет, Хабр! Я работаю в 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, обмениваемся опытом и даже устраиваем живые митапы. Пишите свои плагины и делитесь результатами! Присоединяйтесь:

ссылка на оригинал статьи https://habrahabr.ru/post/273179/