Пишем свой 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, 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 будет искать помеченные классы. Но это пока только задумка.
    Как всегда буду рад любом новым идеям и предложениям. Ждите обновления в ближайшее время.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 13

      –1
      Небольшое пожелание Вам — неплохо бы начать с пояснения, что такое ORM (для примера вот эта статья). А то как-то даже непонятно, нужно ли мне это читать или нет.
        0
        В принципе, пожелание прекрасно понимаю, но вот как это вписать в то, что пишу частями — нет! Не писать же в начале каждой части одно и тоже? В общем, постараюсь в следующий раз как-нибудь это обыграть.
        0
        Судя по текущим доработкам, скоро придется писать Часть 3.1!
          0
          Сколько этих ORM для Android не пересмотрел, пришел к выводу, что руками и код более производительный и реализация очевидная. Да рутина, но зато везде как надо, нет наследования ненужных классов и прочей лабуды.

          P.S. Это не упрек вашей реализации, просто мысли вслух
            0
            Я сам об этом и говорил в первой статье, сравнивая наиболее популярные в тот момент orm’ы. Потому и принялся за свою реализацию, максимально упрощающую как проектирование модели (без всяких там генераторов сущностей и написания лишних классов), так и работу с базой. Для меня orm — это побочный продукт разработки основного приложения (как и набор Control’ов), но из него неплохо получается и отдельный проект, который, возможно, когда-нибудь будет удобнее использовать чем прямую реализацию.
              0
              Один мой знакомый плюнул и решил написать человеческий генератор SQL запросов, что на мой взгляд более лучшее решение для Android. То есть всю логику работы с данными вы пишете сами, но SQL запросы можно нагенерить библитечкой, при этом производительность и читаемость кода остаются прежними.
                0
                Ну, производительность все же снизиться должна, пусть и не на столько как от orm, ведь sql запрос не вшит в код, а собирается. В любом случае, интересно было бы посмотреть на код, а то может как-нибудь и удастся соединить два продукта в один.
            0
            Кстати, у вас в README в Application классе используется

            @Override public void onTerminate() { OrmFactory.ReleaseHelper(); super.onTerminate(); }

            он никогда не вызывается на устройствах, так что смысла в нём нет.
              0
              Да, большое спасибо! Стащил реализацию из OrmLite, не посмотрев, что к чему. Однако, базу все равно надо бы закрывать, так что изучу, как лучше переделать.
              0
              Почему просто не взять какой ть схема-лесс субд движок типа ориентдб? Зачем весь вот этот ад в 2014 году?
                0
                Уже обсуждалось в этой ветке комментариев.
                0
                Уберите статью из хаба «Android», пожалуйста. Те, кому эта статья может пригодится, и так её увидят, потому что подписаны на хаб «Разработка под Android»
                  0
                  Убрал.

                Only users with full accounts can post comments. Log in, please.