Pull to refresh

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

Reading time 5 min
Views 5.5K

Вступление


После некоторого перерыва в разработке моего приложения под 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, List<OrmEntity> 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).renameColumn("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 будет искать помеченные классы. Но это пока только задумка.
Как всегда буду рад любом новым идеям и предложениям. Ждите обновления в ближайшее время.
Tags:
Hubs:
+8
Comments 13
Comments Comments 13

Articles