Привет! Я работаю в Mail.Ru Group в отделе разработки плагинов JIRA. Плагины позволяют расширять или изменять функциональность приложения. Например, с их помощью можно создавать новые типы полей, гаджеты, JQL-запросы, панели с различной информацией, графики и многое другое.
Большинство наших плагинов требуют хранения дополнительных данных, которые они используют. В этой статье я хочу рассказать, как мы решаем эту задачу. Существует два основных способа хранения таких данных: Active Objects и Plugin Settings. Рассмотрим их поподробнее и разберемся в каком случае лучше и удобнее использовать один, а в каком — другой.
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, они являются простым, быстрым и масштабируемым способом хранения и доступа к информации. Данные хранятся в отдельной таблице в базе данных. Объекты можно связывать друг с другом.
Используемые источники:
- Документация по Active Objects от Atlassian
- Введение в плагин Active Objects
- Документация по Plugin Settings от Atlassian
- Как и где храняться Plugin Settings
P. S. У нас есть профессиональное сообщество в социальных сетях, где мы обсуждаем использование продуктов Atlassian, обмениваемся опытом и даже устраиваем живые митапы. Пишите свои плагины и делитесь результатами! Присоединяйтесь:
Автор: Mail.Ru Group