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>
</a>

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



Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.