Пишем свой Orm под Android с канастой и сеньоритами, Часть 3-я

от автора

Вступление

После некоторого перерыва в разработке моего приложения под Android, в течении которого в моей голове формировались все новые и новые идеи, как сделать его красивее и удобнее, в конце января я вновь уселся за разработку. За время размышлений подход к созданию приложения немного трансформировался, и посему до объектной модели я добрался только тройку недель назад. И почти сразу столкнулся с необходимостью доработки UCAOrm. Кому интересно узнать не только об уже внедренных нововведениях, но и о том, что еще только в процессе разработки —

Изменения и дополнения

Первое с чем я столкнулся: это необходимость в ContentProvider’е и в Cursor’е.
С ContentProvider’ом проблем особо не возникло — абстрактный OrmContentProvider наследуется от ContentProvider’а и реализует пока два метода: query, принимающий OrmWhere и возвращающий OrmCursor, и update, принимающий обновляемый экземпляр. OrmCursor же наследуется от AbstractCursor и, кроме реализации всех необходимых методов, реализует еще и метод getEntities — возвращающий List объектов. Самыми же интересными, с точки зрения реализации, являются функция getColumnNames, которая возвращает массив имен колонок (функцию getOrmFields уже переделал), и приватная функция getObject, возвращающая значение указанной колонки. Данные классы намного упростили разработку аккаунта синхронизации.
Вторым нововведением стала поддержка новых типов полей: boolean и int array. Если с boolean все более-менее понятно, то про array расскажу немного подробнее. Сначала появилась идея создавать дополнительную таблицу с именем “имя класс_имя поля” и одном единственным столбцом типа компонента массива. Однако, порассуждав, пришел к выводу, что массив с классом, наследуемым от OrmEntity, рушит всю архитектуру, а любой другой не примитивный тип разработчику все равно придется сериализавать вручную. Отсюда и решил, что orm будет поддерживать только массивы примитивных типов, которые отлично сериализуются в строку и также отлично десериализуются обратно. Проблемы, разве что, могут возникнуть с double, формат которого в виде строки может содержать запятую, являющуюся разделителем элементов массива, но они легко решаются жесткой установкой локали в English.
Так же, наконец добрался до реализации метода getDefaultValues в наследнике OrmHelper’а. Теперь он выглядит так:

    @Override     public void getDefaultValues(Class<? extends OrmEntity> entityClass, List<OrmEntity> valueList) {     } 

соответственно, добавление значений по умолчанию для нашей любимой модели из второй части будет реализовано так:

   public void getDefaultValues(Class<? extends OrmEntity> entityClass, ArrayList<String> columns, ArrayList<ContentValues> valueList) {         if (entityClass.equals(CarType.class)) {             valueList.add(new CarType("Passenger"));             valueList.add(new CarType("Truck"));         }     } 

Ну, а теперь мы подобрались к самой вкусной проблеме, о которой говорил hardex еще в первой статье — обновление схемы данных.

Обновление схемы данных

Опять же, вернемся к нашей модели и рассмотрим сущность Car:

    @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;     } 

Предположим, что мы решили добавить еще одно поле:

       @Column(name = "max_speed")        private int maxSpeed; 

В этом случае нам надо изменить версию базы в manifest’е:

<meta-data android:name="UO_DB_VERSION" android:value="2" /> 

И написать код в методе onUpdate helper’а:

   @Override     protected void onUpgrade(int oldVersion, int newVersion) { 	    if (newVersion == 2) { 		    OrmUtils.UpdateTable(Car.class).work(); 	    }     } 

А зачем еще нужен метод work?” — спросит кто-то. А давайте рассмотрим возможные варианты изменения схемы данных:

  1. В схему добавляется новое поле.
  2. Из схемы удаляется поле.
  3. Поле переименовывается.
  4. У поля изменяется тип.

Скорее всего, многие уже догадались, что единственный пункт, не вызывающий сложностей — первый, но рассмотрим их по порядку.

Добавления поля

Тут все легко: orm выгребает поля из таблицы и сравнивает с полями из класса. Когда находится новое поле в объектной модели, для него дергается

ALTER TABLE … ADD COLUMN … 

Если нужно будет значение по умолчанию, то его нужно будет указать в аннотации.

Удаление поля

Начало алгоритма схоже с предыдущим: сравниваем поля и находим те, которые надо удалить. Ну, а дальше, почти, как указано в faq’е. Единственно, не понимаю, зачем нужно второе копирование, ведь после того, как drop’нули таблицу, временную можно просто переименовать, и она станет постоянной!

Переименование поля

А вот тут work вам не помощник! Orm просто-напросто не поймет, что вы просто переименовали поле, и сделает два действия: удалит поле со старым именем из базы и добавит новое с новым. Конечно, это так же можно было обыграть в аннотации, добавив поле old_name, но мне показалось, что это уже слишком, да и orm можно разгрузить, точно указывая ему, что делать. В свете вышеизложенного в данном абзаце, нам нужен метод rename:

OrmUtils.UpdateTable(Car.class).rename("old_column_name", "new_column_name"); 

Заметьте, что нужно указывать именно имя колонки, а не поля! В результате, orm не будет шерстить весь класс и все поля в базу, чтобы понять, что ему надо изменить, а просто сделает изменения имени одной единственной колонки.
Так же, мы можем помочь ему добавить колонку:

OrmUtils.UpdateTable(Car.class).addColumn("column_name"); 

и удалить колонку:

OrmUtils.UpdateTable(Car.class).deleteColumn(“column_name”); 

Само же переименование в виде sql запроса вызвало некоторые вопросы. Сначала я решил это, как и удаление, созданием новой таблицы с нужным именем поля, куда копируются данные из старой, и она просто удаляется, а новая — переименовывается. Но потом, я наткнулся на эту статью и планирую попробовать этот метод.

Изменение типа поля

Orm опять же все может сделать за Вас, но можно ему и помочь:

OrmUtils.UpdateTable(Car.class).changeColumnType("column_name"); 

В принципе, вторым параметром можно было бы еще передать и новый тип колонки, но давать возможность программисту указать не тот тип, а потом ругать orm (:-)) мне не хотелось. Проблему же несовместимости данных старого типа и нового решает сама база, бросив исключение при копировании старой таблицы в новую. Но колонку можно и просто обнулить, передав в качестве второго параметра true:

OrmUtils.UpdateTable(Car.class).changeColumnType(“column_name”, true); 

Еще в замысле был параметр, который указывает на то, что обнулить поле надо только в случае несовместимости типов, но пока делать не стал.

Заключение

Вот таким изменениям подвергся UCAOrm за последние две недели. В github выложено еще не все, так как, как и писал чуть выше, работа над Updater’ом еще ведется, и еще не все протестировано. Так же есть задумка немного упростить первоначальное создание таблиц: просто вызвав метод createByPackeg у OrmUtils, передав туда имя пакета, в котором orm будет искать помеченные классы. Но это пока только задумка.
Как всегда буду рад любом новым идеям и предложениям. Ждите обновления в ближайшее время.

ссылка на оригинал статьи http://habrahabr.ru/post/216419/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *