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