Пользователь
0,0
рейтинг
9 июля 2013 в 17:25

Разработка → Пишем свой Orm под Android с канастой и сеньоритами из песочницы

Вступление


Идея написать свое приложение под Android пришла мне на пятый день отдыха в солнечном Таиланде. Не буду вдаваться в подробности что именно натолкнуло меня на неё, как и что я задумал за приложение (просто статья не об этом). Однако идея крепко укоренилась и на шестой день пребывания, воспользовавшись бесплатным интернетом в отеле, на ноутбук, взятый только ради просмотра фильмов и скидывания фотографий с фотоаппарата, я закачал MySql.
Начал я, как вы наверное уже догадались, с реляционный модели.
Работа шла трудно, но через пару месяцев с моделью я закончил и окунулся в дебри разработки под Android. До этого, под мобильные платформы я писал только на .Net Compact Framework, но так как с Java был знаком не понаслышке, накидать простенькую форму с кнопками труда не составило. Объектная модель, ожидаемо, трудностей не вызвала вообще и я, радостно предвкушая как сейчас мои тестовые данные улетят куда-то в недра устройства, открыл раздел Data Storage на сайте Android Developers. Раздел Using Databases нельзя назвать исчерпывающим, однако все необходимы ссылки на API он содержит, и я принялся писать своего наследника от SQLiteOpenHelper. После пары удачных проб, разбалованный Entity Framework’ом, я понял, что и тут бы было неплохо использовать какой-нибудь orm, так как сущностей у меня набралось больше десятка. Вбив в Великом и Ужасном «android orm», первую же ссылку я получил на эту статью, и несколько полезных на StackOverflow. Набрав в общей сложности три orm’а, я приступил к экспериментам.

Эксперименты


Первым подопытным стало конечно OrmLite.
OrmLite

В принципе неплохая реализация, использование аннотации. Еще мне понравилась реализация onCreate и onUpgrade в DatabaseHelper и она запомнилась где-то в глубинах моей памяти. Но! Создавать для каждой сущности еще и дополнительный класс <%EntityName%>DAO – увольте! Можно кончено попытаться сделать один, прицепляемый к базовому классу сущностей, но проблем от этого будет только больше.

GreenDAO

Дальше заголовка «Data model and code generation» в документации я читать не стал. Может быть конечно такой подход мне не совсем понятен и в нем есть свои плюсы, но, привыкнув к атрибутам, использовать аннотации мне нравиться больше.

ActiveAndroid

На данный orm я возлагал большие надежды: аннотации, создание базы с использование manifest’а (запомнилось в тех же глубинах что и Helper OrmLite), маппинг через методы самой сущности… В общем вроде всем данный framework хорош, но потянуть мою реляционную модель он не смог.

Реляционная модель


Немного отступлю от orm, чтобы наконец объяснить, что за реляционная модель у меня получилась. В принципе, все в ней конечно стандартно, за исключением одной вещи. В Entity Framework’е, при маппинги наследуемых сущностей создается одна таблица с избытком полей. Например: предположим у нас есть сущность «Машина»
/**
 * Машина
 */
public class Car {

    /**
     * Тип
     */
    private CarType type;
    
    /**
     * Мощность двигателя
     */
    private int enginePower;

    /**
     * Кол-во дверей
     */
    private int doorsCount;
}

И наследуемая от неё сущность «Грузовик»
/**
*Грузовик
*/
public class Truck extends Car{

    /**
     *Указвает на то, что это самосвал.
     */
    private boolean isTipper;
}

Обычно orm должен сгенерировать такой запрос на создание таблицы:
CREATE TABLE car (id INTEGER PRIMARY KEY, type INTEGER REFERENCE car_type(id), engine_power INTEGER, doors_count INTEGER, is_tipper INTEGER);

Я же, в свое время впечатленный наследованием таблиц в PostgreSQL, сделал такую модель:
CREATE TABLE car (id INTEGER PRIMARY KEY, type INTEGER REFERENCES car_type(id), engine_power INTEGER, doors_count INTEGER);
CREATE TABLE truck (id INTEGER REFERENCES car(id) ON DELETE CASCADE,  is_tipper INTEGER);

Сразу уточню, что это только пример, и в моих сущностях, полей и связей намного больше, а посему забивать таблицу Car, тем, что к ней не относиться – я не хотел.
В результате выборку надо делать через Left Join.
В результате, я решил изобрести велосипед, сиречь написать свой собственный Orm.

Концепция


  1. Никаких генераторов сущностей! Все через аннотации.
  2. Как можно меньше разных типов аннотаций, в идеале две (пока удается этому придерживаться).
  3. Запрос сущностей через статические методы самого класса.
  4. Создание, сохранение и удаление – через методы экземпляров.
  5. Максимально простой Helper с возможностью создания начальных записей таблиц.


Реализация


Аннотации

Аннотация у меня всего две:
  1. Table – применяется к классу, описывает таблицу. Свойства:
    • Name – имя таблицы, если пустое – то берется имя класса в нижнем регистре.
    • CashedList – указывает, надо ли для экземпляров данного класса загружать каждый заново, или искать его в закешированном списке (объясню чуть позже).
    • LeftJoinTo – указывает класс к которому данный является наследником.
    • RightJoinTo – указывает классы, которые расширяют текущий.
  2. Column – применяется к полю, описывает столбцы таблицы. Свойства:
    • Name – имя столбца, если пустое – то берется имя поля в нижнем регистре.
    • PrimaryKey – указывает что данное поле ключевое.
    • Inherited – указывает, что данная аннотация наследуется.
    • ReferenceActions – список действий для References поля.


Сущности

Опять же будем рассматривать их из примера:
public class BaseEntity extends OrmEntity {

	@Column(primaryKey = true, inherited = true)
	private Long id;
}

@Table(name = “car_type”, cashedList = true)
public class CarType extends BaseEntity  {

	@Column
	private String code;
}

@Table(rightJoinTo = {Truck.class})
public class Car extends BaseEntity {

	@Column(name = “car_type”)
	private CarType type;

	@Column
	private List<Wheel> wheels;

	@Column(name = “engine_power”)
	private int enginePower;

	@Column(name = “doors_count”)
	private int doorsCount;
}

@Table
public class Wheel extends BaseEntity {

	@Column(name = “car_id”)
	private Car car;

	@Column
	private String manufacturer;
}

@Table(leftJoinTo = Car.class)
public class Truck extends Car {

	@Column(name = “is_tipper”)
	private boolean isTipper;
}


В результате работы Helper’а похожего на Helper OrmLite получим следующие запросы:
CREATE TABLE car_type (id INTEGER PRIMARY KEY, code TEXT);
CREATE TABLE car (id INTEGER PRIMARY KEY, car_type REFERENCES car_type (id), engine_power INTEGER, doors_count INTEGER);
CREATE TABLE wheel (id INTEGER PRIMARY KEY, car_id INTEGER REFERENCES car (id), manufacturer TEXT);
CREATE TABLE truck (car_id REFERENCES car (id), is_tipper INTEGER);


Дополнительно по orm

Класс OrmEntity имеет защищенный статический метод:
protected static <T extends OrmEntity> List<T> getAllEntities (Class<T> entityClass)


Например, в классе CarType реализуем такой метод:
public static List<CarType> getAllEntities() {
	return getAllEntities(CarTpe.class);
}


Так же в OrmEntity есть защищенные методы alter и delete. В базовом классе BaseEntity я прячу alter, и дергаю его через insert и update, ну так, чисто для красоты.

Проверки и ограничения

У сущности обязано быть поле помеченное primaryKey. Оно должно быть обязательно типа Long (именно Long, что бы изначально id было null’ом и alter смог понять, что это, insert или update).
Если поле помеченное Column имеет тип List с классом сущностей унаследованных от OrmEntity, то в этом классе обязательно должно быть поле типа первого класса помеченное Column.
Если поле помеченное Column имеет тип унаследованный от OrmEntity, то у этого типа обязано быть поле помеченное Column с primaryKey.
Если класс помечен Table с leftJoinTo, то у класс указанный в leftJoinTo, должен быть помечен Table в rightJoinTo которого добавлен первый класс.
Все не пройденные проверки бросают Exception.

Заключение


В текущей момент orm еще не закончен, и я развиваю его по мере моих надобностей. Сейчас поддерживаются типы int, Long, double, String, Drawable. На подходе Document.
Из действия по references пока есть только ON DELETE по нему можно сделать и CASCADE и SET NULL и остальные.
LeftJoinTo – пока работает не полностью, сейчас доделываю как раз его.
Как только доделаю все задуманное и причешу код – выложу библиотеку на GitHub.
@Scogun
карма
6,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (11)

  • 0
    интересно как вы тащите сущности из таблицы, в которой мноого записей используя пресловутый статический метод getAllEntities без условий на выборку (ну например для пейджинга по таблице с фильтрами и группировками)? Тут напрашивается свой парсер критериев выборки (свой язык в общем-то) имхо (ну не where лепить же, который зависимый от конкретной субд)…
    • 0
      Ну, конечно нет. getAllEntities это только один из примеров. Хотя отчетами еще не занимался, и в полноценном where смысла пока не было, но сейчас еще как минимум есть getEntityByKeyValue и getEntitiesByParent. Думаю, по поводу первой функции и так все ясно, а вот вторая принимает на вход, например, экземпляр Car, а возвращает все связанные с ним Wheels. Тут опять же, еще не универсально, так как пока у меня нет сущностей содержащих несколько связанных списков. Where же хочу реализовать в стиле Android (как, например AlertDialog.Builder), то есть каждый его оператор будет возвращать все тот же конструктор. Что-то типа этого:
      Car.Where().Equils(”engine_power”, 169).And().Equils(“doors_count”, 5).Select();
      

  • 0
    В свое время тоже делал для себя клон ActiveAndroid, но не ушел достаточно далеко :)
    В настоящих приложениях реальную проблему представляют собой миграции схемы при обновлениях.
    • 0
      Да. Я обновлениями пока тоже не занимался, хотя и предусмотрел onUpgrade в Helper’е. На самом деле, orm составляет только лишь 30% от моего проекта, это, так сказать, сопутствующая вещь. А посему и хочу сделать его открытым, вот только код причешу немного.
  • 0
    При желании хранить сущности-наследники одного класса с большой вариацией наборов полей я бы выбрал документно-ориентированную бд. Сразу скажу, что не знаю, как с этим у Андроида.
    • 0
      Нативно Android поддерживает только SQLite. И хотя цепляться к удаленным базам проблем не составляет (пробовал к MySQL установленной на сервере – все хорошо), установить другой движок БД именно на устройство, думаю, будет весьма проблематично. Плюс, работал с XML в Oracle, вроде все отлично: и преобразования есть, и валидация, и XPath поддерживается в полной мере. Но, как только записей с XML данным внутри которых необходимо провести поиск переваливает хотя бы за 1k, время запроса увеличивается в разы. Пришлось городить огород из дополнительных таблиц для поиска. Хотя, возможно, это из-за того, что изначально Oracle не документо-ориентированная СУБД.
      • –1
        А что мешает тащить любую embedded database (будь то NoSQL, key-value, whatever) в виде нативных бинарников под нужные архитектуры?
        • 0
          И распирать apk-файл до десятка мегабайт? Я уже получил проблему в самом приложении, оттого, что в Google не реализовали SchemaFactory, и провести валидацию XML без сторонних библиотек не получается. Библиотек Xerces же, увеличивает apk на 2.2 Мб! Пришлось писать свою валидацию (без поддержи xsd, разумеется). Плюс, сама SQLite меня совершенно устраивает.
          • 0
            Если у Вас жесткие ограничения на размер apk, тогда понятно. Просто заявление «установить другой движок БД… проблематично» — очень уж сильное :)
            • 0
              Ограничения конечно не жесткие, но факт – остается фактом. Хотя с «проблематично», я может быть слегка и погорячился.
        • 0
          Думаю, что самопальная сборка какого бы то ни было хранилища на неподдерживаемой платформе может вылиться в те ещё баги. Хотя, опять же, я не спец по Андроиду.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.