Вступление
После некоторого перерыва в разработке моего приложения под 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?” — спросит кто-то. А давайте рассмотрим возможные варианты изменения схемы данных:
- В схему добавляется новое поле.
- Из схемы удаляется поле.
- Поле переименовывается.
- У поля изменяется тип.
Скорее всего, многие уже догадались, что единственный пункт, не вызывающий сложностей — первый, но рассмотрим их по порядку.
Добавления поля
Тут все легко: 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 будет искать помеченные классы. Но это пока только задумка.
Как всегда буду рад любом новым идеям и предложениям. Ждите обновления в ближайшее время.
Автор: Scogun