Що ORM тобі моєму? Один навколонауковий підхід вибору ORM для Android

Вибір інструментів, які так чи інакше знадобляться при розробці — один з головних підготовчих етапів на старті нового Android-проекту.
У разі, якщо ви розробляєте додаток, яке повинно в тому чи іншому вигляді зберігати велику кількість сутностей — вам не уникнути використання баз даних. На відміну від колег по цеху, які розробляють для iOS, Android-програмістів немає зручних інструментів, що полегшують зберігання об'єктів начебто Core Data, що надаються платформою (крім Content Provider, про те чому він не в рахунок, буде далі). Тому багато Android-розробники вдаються до використання сторонніх ORM рішень у своїх проектах. Про те, на що варто дивитися при виборі бібліотеки для вашого проекту, і піде мова в цій статті.



Для початку хотілося б переконатися, що вибір ORM не надуманий і ми розглянули всі доступні засоби для зберігання даних, які надаються Android SDK «з коробки».

Розглянемо їх по мірі зростання складності написання реалізації такого сховища. Зауважте, що я спеціально не став розглядати звичайні файли у цьому порівнянні. Вони, звичайно, є ідеальним варіантом для любителів власних велосипедів, які живуть з філософії Not Invented Here, але в нашому випадку вони будуть занадто низькорівневими.

Shared Preferences
http://developer.android.com/guide/topics/data/data storage.html#pref
Сховище типу ключ-значення для примітивних типів даних. Підтримуються
Integer, Long, Float, Boolean, String і StringSet. Основне призначення — зберігання якогось стану програми налаштувань. По своїй суті являє обгортку над XML-файлом, який знаходиться в «приватній» папці вашого застосування в піддиректорії shared-prefs. Для зберігання безлічі однотипних структурованих даних не підходить.

Бази даних SQLite
http://developer.android.com/guide/topics/data/data storage.html#db
SQLite є стандартною базою даних в Android. У фреймворку надані кілька класів помічників, які полегшують роботу з базою: SQLiteOpenHelper, ContentValues і т. д. Однак, навіть використання цих помічників не позбавить вас від обов'язку писати величезну кількість шаблонного коду, самостійно стежити за створенням і зміною таблиць, створювати методи для операцій, методи пошуку і т. д. Таким чином, код додатків, що використовують тільки стандартні інструменти для роботи з SQLite Android, стає все важче підтримувати при додаванні нових і зміну старих сутностей.

Content Provider
http://developer.android.com/guide/topics/providers/content-providers.html
Content Provider є прошарком над реальним сховищем даних. Може здатися, що Content Provider є «коробкової» реалізацією технології ORM, однак це далеко не так. Якщо ви використовуєте SQLite в якості сховища для Content Provider, вам доведеться самостійно реалізувати логіку створення, оновлення таблиць і базових CRUD операцій. У більшості випадків використання Content Provider без спеціальних генераторів не тільки не заощадить час на розробці та підтримці, але, можливо, і витратить його куди більше, ніж написання своєї реалізації SQLiteOpenHelper. Однак, Content Provider дозволяє використовувати деякі зручні класи платформи — такі як AsyncQueryHandler, CursorLoader, SyncAdapter та інші.

Переконуємося, що ми розглянули всі доступні в Android SDK інструменти зберігання даних і приходимо до висновку: SQLite забезпечує всі необхідні умови для організації сховища однотипних структурованих даних (дивно, чи не правда?). Однак, як зазначалося вище, використання SQLite Android вимагає великої кількості коду і постійної підтримки, тому спробуємо полегшити своє життя, вдавшись до стороннього рішенням.

Тут на допомогу якраз і приходить техніка ORM — Object Relational Mapping. Її реалізація, по суті, створює враження об'єктної бази даних, маючи у своїй основі звичайну реляційну базу даних. ORM, надаючи більш високий рівень абстракції, покликане позбавити програмістів від необхідності конвертувати об'єкти моделі даних в скалярні величини, які підтримуються базою даних, дозволити їм писати менше шаблонного коду і не турбуватися про структуру таблиць.

Визначившись з технологією, звернемося з таким питанням в інтернет і виберемо 4 бібліотеки:
Як вибрати потрібну бібліотеку і не пошкодувати про своє рішення, якщо буде пізно? В аналогічних статтях я натрапив тільки на якісні порівняння бібліотек. Однак, на мій погляд, ORM-бібліотека повинна бути збалансована в плані зручності і продуктивності. Тому порівняння цих рішень тільки з точки зору API, без аналізу продуктивності, було б неповним. Але для початку невеликий відступ про те, чому все ж варто звертати увагу на продуктивність ORM.

Навіщо все це?

Навіщо оцінювати продуктивність ORM? Очевидно ж, що в кінцевому рахунку все упреться в обмеження самої SQLite, а та, у свою чергу, обмеження файлової системи (мова йде про single-file базі даних). Однак, як з'ясувалося, до цих природних обмежень ще дуже далеко.

Перед тим, як перейти до опису проведеного мною тесту і його результатами, хотілося б розповісти невелику історію про те, чому ми стали звертати увагу на продуктивність ORM, яку використовуємо в своїх проектах і спробах її поліпшити.

Одного разу до нас в Sebbia надійшло на розробку якесь додаток, яке споживає уніфіковане для всіх клієнтів REST API. З усіх існуючих на ринку ORM було вирішено використовувати перевірений часом і нас повністю задовольняє на той момент ActiveAndroid. Основною сутністю програми (для простоти назвемо її «Сутність») є певний стан безлічі інших сутностей системи, частини яких («Власники сутності») були представлені тільки ідентифікаторами цих сутностей. Передбачалося, що при запиті «Сутності» клієнт буде завантажувати «Власників сутності» автоматично, якщо вони не були виявлені в кеші програми. У мобільних пристроях такій ситуації хотілося б уникнути — у вигляді витрат енергії на відправку нового запиту. Які-небудь зміни в API призвели б до потенційних проблем сумісності з іншими клієнтами. Тому ми вирішили завантажувати і кешувати список Власників сутності" до того, як виникне необхідність завантажувати самі «Сутності». Найлогічніше таку операцію виконувати при першому запуску програми. Варто обмовитися, що список усіх Власників сутності" віддавався повністю, а не посторінково. Яким же було наше здивування, коли ми побачили те як довго зберігається цей список в базу даних!

Перевіривши код ще раз, і переконавшись, що список зберігається один раз і усередині транзакції, під підозру потрапив ActiveAndroid. Загалом, причиною падіння продуктивності програми при збереженні об'ємного списку була рефлексія, а саме — отримання значень полів об'єкта і заповнення ними ContentValues. Замінивши код, що використовує рефлексію, на код, згенерований самописным плагіном для Eclipse, ми отримали майже дворазовий приріст продуктивності — c ~38 секунд до 20-ти. Переконавшись зайвий раз в тому, що варто уважніше дивитися на те, як влаштовані open-source бібліотеки зсередини, перейдемо до змістовної частини статті.

Гонка озброєнь

З усіх вибраних бібліотек окремо стоїть GreenDao — адже він єдиний серед представлених рішень, хто використовує кодогенерацию. Щоб не робити швидких висновків — GreenDao швидше за всіх, а про решту забудьте, ми вирішили оформити підхід з кодогенерацией (використаний в описаному вище проекті в ActiveAndroid) у вигляді окремого форк і pull request в офіційний репозиторій, паралельно доповнивши його іншими корисними функціями: підтримкою відносин «одне до чого», «багато до чого» і автоматичної міграції даних при зміні структури сутностей і, відповідно, їх таблиць. Отриманий форк, який для простоти я буду називати “ActiveAndroid.Sebbia", був доданий до тесту.

Настав час розповісти про проведений тесті. Він перевіряє, як швидко та чи інша бібліотека може зберігати тестові сутності в ході SQLite транзакції і здійснює зворотну операцію. У новостворену базу даних додаються 1000 тестових об'єктів, які після очищення кешу в пам'яті ORM зчитуються і перевіряються на коректність даних. Кожен випробуваний перевіряється 10 разів, за кінцевий результат береться середнє час виконання. Тестові об'єкти складаються з двох текстових полів фіксованої довжини, одного поля дати та одного масиву байт, отриманого з сериализуемого об'єкта. Спочатку передбачалося, що ORM повинна була сама перетворювати Serializable об'єкт в масив байт, але виявилося, що ні в GreenDao, ні в SugarORM немає такої можливості, тому від цієї ідеї довелося відмовитися.

Для того, щоб зрозуміти максимально можливу швидкість операції «об'єкт-рядок таблиці-об'єкт», якої можна досягти за допомогою стандартних засобів Android SDK, в порівняння був доданий приклад використовує SQLiteOpenHelper і скомпільовані SQLiteStatement. Сам проект і всі бібліотеки версій, на основі яких було вироблено порівняння, розташований на GitHub.


Результати досить передбачувані, No ORM-рішення швидше за всіх, хоча воно випереджає GreenDAO зовсім не на багато. За великим рахунком, код цих рішень однаковий, за винятком того, що GreenDAO надає більш зручний інтерфейс.

У свою чергу, GreenDAO займає друге місце в загальному заліку, але перше серед ORM. І це не дивно. GreenDAO — єдине ORM, повністю використовує кодогенерацию, скомпільовані SQLiteStatement'и та інші оптимізації.
На друге місце серед ORM і третє в загальному заліку виходить ActiveAndroid.Sebbia — наш форк ActiveAndroid, використовує кодогенерацию, що працює через Annotation Processor, і SQLiteStatement. Операція запису виконалася майже в 4 рази швидше у порівнянні з оригінальним проектом, однак читання вдалося оптимізувати незначно.

ActiveAndroid в заліку ORM третій, потім слід ORMLite-ORM, що прийшла в Android-світ з «великої» Java, що має кілька плагінів для роботи з різними джерелами даних і досить зручна в роботі. На останньому місці знаходиться SugarORM — сама, на мій погляд, невдала з розглянутих. По-перше, остання доступна версія з master-гілки не підтримувала збереження масиву байт, довелося виправити це непорозуміння і перезібрати бібліотеки, причому на GitHub'e проекту вже давно перебуває pull request, це додає функцію. По-друге, SugarORM створює враження дуже сильно урізаного у функціональному плані клону ActiveAndroid (відсутність можливості конвертувати об'єкти інших класів і адаптерів).

Добре, з продуктивністю розібралися — кодогенерация швидко, рефлексія повільно. Виклик SQliteDatabase.insert(...) повільніше ніж виклик заздалегідь створеного SQLiteStatement. Але наскільки, все ж таки, зручно використовувати ці бібліотеки? Зупинимося на кожній докладніше.

Зручності у дворі

Почну огляд API представлених бібліотек з чемпіона — GreenDAO.
Як вже говорилося вище, проект досить незвичайний порівняно з іншими ORM. Замість того, щоб створювати класи сутностей самостійно і вказувати поля, що зберігаються в таблиці, GreenDAO пропонує складати ці класи з іншого коду, отриманий код генерації необхідно запускати як звичайне Java додаток. Ось як приблизно це виглядає:

public static void main(String[] args) throws Exception {
Schema schema = new Schema(1, "com.sebbia.ormbenchmark.greendao");
schema.enableKeepSectionsByDefault();
Entity entity = schema.addEntity("GreenDaoEntity");
entity.implementsInterface("com.sebbia.ormbenchmark.BenchmarkEntity");
entity.addIdProperty().autoincrement();
entity.addStringProperty("field1");
entity.addStringProperty("field2");
entity.addByteArrayProperty("blobArray");
entity.addDateProperty("date");

new DaoGenerator().generateAll(schema, "../src-gen/");
}

Якщо вам необхідно додати свої поля в класі сутності, вам необхідно помістити їх в блоки, відмічені спеціальними коментарями:

// KEEP FIELDS - put your custom fields here
private Blob blob;
// KEEP FIELDS END

Аналогічні блоки коментарів передбачені і для методів, і для імпорту. Якщо не брати до уваги приголомшливу продуктивність такого підходу, то його зручність вкрай сумнівно, особливо якщо використовувати GreenDAO з першого дня розробки. Однак питання виникають і при використанні вже згенерованого коду. Наприклад, навіщо потрібно писати стільки, щоб отримати DAO об'єкт:

DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(context, "greendao", null);
DaoMaster daoMaster = new DaoMaster(devOpenHelper.getWritableDatabase());
DaoSession daoSession = daoMaster.newSession();
GreenDaoEntityDao dao = daoSession.getGreenDaoEntityDao();

Мені здається, що це занадто. Зрозуміло, що створювати DaoMaster кожен раз не знадобиться, але все ж. Отже, з GreenDAO вам доведеться виконувати зайві рухи тіла для підтримки коду і використовувати не саме вдале API. Однак натомість ви отримуєте швидкість і приємні бонуси на зразок підтримки Protobuf об'єктів з коробки.

Перейдемо до ORMLite. ORMLite пропонує активно використовувати анотації при оголошенні своїх сутностей:

@DatabaseTable(tableName = "entity")
public class OrmLiteEntity implements BenchmarkEntity {
@DatabaseField(columnName = "id", generatedId = true)
private long id;
@DatabaseField(columnName = "field1")
private String field1;
@DatabaseField(columnName = "field2")
private String field2;
@DatabaseField(columnName = "blob", dataType = DataType.BYTE_ARRAY)
private byte[] blobArray;
@DatabaseField(columnName = "date", dataType = DataType.DATE)
private Date date;
}

Через анотації можна задати тип даних поля, що дуже зручно і не розмазує код, пов'язаний з моделлю за проектом. Проект підтримує безліч типів даних і варіантів їх зберігання. Наприклад, для java.util.Date передбачений як числовий, так і рядковий варіант. До недоліків можна віднести необхідність реалізовувати OrmLiteSqliteOpenHelper, через який ви зможете отримати DAO об'єкт і взаємодіяти з ORM. Використання окремих об'єктів DAO позбавляє від необхідності наслідувати класи ваших сутностей від об'єктів сторонніх бібліотек і дозволяє гнучко управляти кешем.

ActiveAndroid використовує схожий підхід з анотаціями, проте вимагає, щоб класи моделі успадковувалися від наданого їм класу Model. На мій погляд, таке рішення оптимально по зручності тільки якщо ваші суті вже не успадковуються від якого-небудь класу, батьків якого ви не можете змінити. Таке спадкування дозволяти мати зручні методи типу save() і delete() у моделі об'єктів без створення додаткових об'єктів DAO. У бібліотеці також надані сериализаторы дат BigDecimal та інших типів, а для серіалізації полів нестандартних типів достатньо реалізувати свій TypeSerializer і вказати його при ініціалізації.

Як вже говорилося вище, Sugar ORM створює враження досить слабкого клону ActiveAndroid. Однак Sugar ORM не вимагає успадкування від абстрактного класу і має досить лаконічне API:

@Override
public void saveEntitiesInTransaction(final List<SugarOrmEntity> entities) {
SugarRecord.saveInTx(entities);
}

@Override
public List<SugarOrmEntity> loadEntities() {
List<SugarOrmEntity> entities = SugarRecord.listAll(SugarOrmEntity.class);
return entities;
}

ActiveAndroid.Sebbia представляє собою форк ActiveAndroid з підтримкою кодогенерации. У цьому проекті генерація коду зв'язування SQLiteStatement і Cursor з об'єктом суті відбувається з допомогою Annotation Processor. Використання Annotation Processor замість плагіна для IDE дозволяє застосовувати його як у Eclipse і в IntelliJ IDEA, так і при складанні проекту з допомогою Gradle або Ant. Однак, це накладає певне обмеження на видимість полів класів моделі: мінімальна допустима видимість в цьому випадку буде без модифікатора (default). Кодогенерация дозволила досягти приблизно 30% приросту продуктивності, все решта — заслуга заздалегідь скомпільованого SQLiteStatement. Також, цей форк містить OneToManyRelation, ManyToManyRelation і підтримку автоматичних міграцій, яка використовується, коли не знайдено SQL-скрипт міграції для поточної версії.

Висновок

У висновку хотілося б підвести підсумок нашого невеликого дослідження. ORM — це корисний інструмент, який допоможе вам зберегти час при розробці ваших додатків. І він абсолютно незамінний у випадку моделі з безліччю елементів і взаємозв'язків між ними.
Варто пам'ятати, що в реальному житті кінцевий користувач, швидше за все, не побачить жодної різниці між найшвидшим і самим повільним ORM, тому варто замислюватися на цей рахунок — вибір кожного. Залишається додати, що рішення, яке ви виберете, повинен бути зручним і відповідати вашим вимогам. У будь-якому випадку, слід дотримуватися загальних правил при виборі open-source бібліотек у ваших проектах, а саме — оцінювати, які відомі програми використовують, яка якість вихідного коду і як вона працює зсередини.

Посилання на репозиторії:
Проект-бенчмарк
ActiveAndroid.Sebbia

Список літературиdeveloper.android.com/guide/topics/data/data storage.html
stackoverflow.com/q/435553/2287859
vaskoz.wordpress.com/2013/07/15/is-java-reflection-slow/
software-workshop.eu/content/comparing-android-orm-libraries-greendao-vs-ormlite
www.sitepoint.com/5-best-android-orms/
github.com/littleinc/android-orm-benchmark
en.wikipedia.org/wiki/Object-relational_mapping


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

0 коментарів

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