ORM в Android c помощью ORMLite

в 17:40, , рубрики: android, greendao, java, java orm, orm, ORMLite, Разработка под android, метки: , , , ,

На данный момент для платформы Android существует несколько решений, позволяющих реализовать ORM-подход для работы с базой данных, но основных два. Это ORMLite и GreenDAO.

Для новичков стоит сделать отступление и рассказать что такое ORM вообще. ORM — object-ralational mapping. Объектно-реляционное отображение означает, что программисту гораздо удобнее оперировать с объектами, которые он использует в своём приложении, нежели чем с таблицами, в которых хранятся данные в реляционных базах данных. Для связи реляционных СУБД с объектной моделью приложения используются ORM-технологии. Так для решения задач объектно-реляционного отображения в Android используют один из сторонних фреймворков. GreenDAO и ORMLite — являются библиотеками с открытым кодом.

GreenDAO

Фреймворк от немецких разработчиков, который они используют в своих приложениях.

Преимущества:

Не использует отражения (Reflection) для работы
Максимальная производительность для Android ORM
Минимальное использование памяти
Маленький размер библиотеки, не оказывает большого влияния на Android проект.

Недостатки:

  • Отсутствует механизм для переноса данных при обновлении схемы.
  • Невозможность аннотирования классов, а необходимость описания алгоритма построения новых сущностей.
ORMLite

Популярный фреймворк для Java, имеющий адаптированную версию под Android.

Преимущества:

  • Мощная и в тоже время простая аннотация
  • Богатый функционал
  • Поддержка многих БД, в т.ч. SQLite
  • Архитектура в соответсвии с принципом KISS

Недостатки:

… хм… я их не находил) всё что кажется не возможным, реализуется при более глубоком изучении доков.

OrmLite я использую в нескольких и проектах и знаком с ним достаточно тесно. Поэтому рассажу как раз о нем.

Простая аннотация классов

Каждый класс, отображение в базу данных (сохрание в БД) которого мы хотим сделать должен быть аннотирован. (можно использовать class-configuration, но это совсем другая история)Стоит заметить, что у класса должен обязательно присутствовать конструктор без аргументов. Пример аннотации:

@DatabaseTable(tableName = "goals")
public class Goal{

   public final static String GOAL_NAME_FIELD_NAME = "name";
   
   @DatabaseField(generatedId = true)
   private int Id;

   @DatabaseField(canBeNull = false, dataType = DataType.STRING, columnName = GOAL_NAME_FIELD_NAME)
   private String name;

  @DatabaseField(dataType = DataType.DATE)
   private Date lastEditDate;

   @DatabaseField()
   private String notes;
   
   public Goal(){
       scheduleList = new ArrayList<Shedule>();
       priorities = new ArrayList<PrioritySchedule>();
   }
}

Первая аннотация @DatabaseTable(tableName = "goals") указывает название таблицы, куда будут отображены объекты этого класса.
Перед каждым полем должна быть аннотация @DatabaseField с различными аргументами. (также можно не указывать аргументов — всё установится по умолчанию, а название столбца в таблице будет совпадать с названием поля). У поля name три аргумента. canBeNull = false означает, что в таблице этот столбец не может быть пустым. dataType = DataType.STRING принудительно обязывает приводить тип столбца к типу String. (доступные типы можно посмотреть здесь). columnName = GOAL_NAME_FIELD_NAME — принудительное указание имени столбца в таблице поможет нам в дальнейшем при построении select-запросов для получения данных из БД.
Первичным ключем может служить любое поле — достаточно указать у него id = true, но рекомендуется сделать автогенерируемое значение id и поставить ему generatedId = true. ORMLite сама назначит ему уникальный номер.
Для индексации столбца в БД указывается index = true
Полное описание всех доступных агументов аннотации полей можно посмотреть здесь
Также помимо аннотаций ORMLite можно использовать привычные некоторым javax.persistence аннотации

Подключение к SQLite в андроид

Существует несколько способов получения доступа к данным БД с помощью ORMLite в андроид. Можно наследовать каждую activity от ORMLiteBaseActivity и тогда нам не придется следить за жизненным циклом соединения с БД, но при этом мы не сможем получить доступа из других классов. (см примеры)
Гораздо предпочтительнее подход, рассмотренный далее.

Нужно создать класс, который будет инстанцировать помощника в создании и работе с БД:

public class HelperFactory{

   private static DatabaseHelper databaseHelper;
   
   public static DatabaseHelper GetHelper(){
       return databaseHelper;
   }
   public static void SetHelper(Context context){
       databaseHelper = OpenHelperManager.getHelper(context, DatabaseHelper.class);
   }
   public static void ReleaseHelper(){
       OpenHelperManager.releaseHelper();
       databaseHelper = null;
   }
}

Обращение к нему будет происходить во время начала и конца жизни приложения:
Это предотвратит утечки памяти из-за незакрытого соединения с БД

public class MyApplication extends Application{

   @Override
   public void onCreate() {
       super.onCreate();
       HelperFactory.SetHelper(getApplicationContext());
   }
   @Override
   public void onTerminate() {
       HelperFactory.ReleaseHelper();
       super.onTerminate();
   }
}

Так же не забываем описать наш класс наследник Application в манифесте:

   <application
       android:name=".MyApplication"
       android:icon="@drawable/ic_launcher"
       android:label="@string/app_name" >

Создадим класс DataBaseHelper, который будет отвечать за создание БД и за получение ссылок на DAO:

public class DatabaseHelper extends OrmLiteSqliteOpenHelper{

  private static final String TAG = DatabaseHelper.class.getSimpleName();

  //имя файла базы данных который будет храниться в /data/data/APPNAME/DATABASE_NAME.db
  private static final String DATABASE_NAME ="myappname.db";
   
  //с каждым увеличением версии, при нахождении в устройстве БД с предыдущей версией будет выполнен метод onUpgrade();
   private static final int DATABASE_VERSION = 1;
   
   //ссылки на DAO соответсвующие сущностям, хранимым в БД
   private GoalDAO goalDao = null;
   private RoleDAO roleDao = null;
   
   public DatabaseHelper(Context context){
       super(context,DATABASE_NAME, null, DATABASE_VERSION);
   }

   //Выполняется, когда файл с БД не найден на устройстве
   @Override
   public void onCreate(SQLiteDatabase db, ConnectionSource connectionSource){
       try
       {
           TableUtils.createTable(connectionSource, Goal.class);
           TableUtils.createTable(connectionSource, Role.class);
       }
       catch (SQLException e){
           Log.e(TAG, "error creating DB " + DATABASE_NAME);
           throw new RuntimeException(e);
       }
   }

   //Выполняется, когда БД имеет версию отличную от текущей
   @Override
   public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVer,
           int newVer){
       try{
        //Так делают ленивые, гораздо предпочтительнее не удаляя БД аккуратно вносить изменения
           TableUtils.dropTable(connectionSource, Goal.class, true);
              TableUtils.dropTable(connectionSource, Role.class, true);
           onCreate(db, connectionSource);
       }
       catch (SQLException e){
           Log.e(TAG,"error upgrading db "+DATABASE_NAME+"from ver "+oldVer);
           throw new RuntimeException(e);
       }
   }
   
  //синглтон для GoalDAO
   public GoalDAO getGoalDAO() throws SQLException{
       if(goalDao == null){
           goalDao = new GoalDAO(getConnectionSource(), Goal.class);
       }
       return goalDao;
   }
   /синглтон для RoleDAO
   public RoleDAO getRoleDAO() throws SQLException{
       if(roleDao == null){
           roleDao = new RoleDAO(getConnectionSource(), Role.class);
       }
       return roleDao;
   }
   
   //выполняется при закрытии приложения
   @Override
   public void close(){
       super.close();
       goalDao = null;
       roleDao = null;
   }
}

Как видно в примере, нужно описать создание полей и таблицы, а также реализовать получение ссылки на синглтон с DAO-объектом. Теперь рассмотрим непосредственно сами DAO.

DAO

Самой простой реализацией является следующий класс

public class RoleDAO extends BaseDaoImpl<Role, Integer>{

   protected RoleDAO(ConnectionSource connectionSource,
           Class<Role> dataClass) throws SQLException{
       super(connectionSource, dataClass);
   }

   public List<Role> getAllRoles() throws SQLException{
       return this.queryForAll();
   }
}

Здесь создан лишь один дополнительный метод для получения коллекции всех объектов класса Role.
Основные методы create, update, delete реализованы в предке BaseDaoImpl.
Для обращения к методом класса RoleDao в любом классе приложения достаточно обратиться к классу-фабрике:
RoleDAO roleDao = HelperFactory.GetHelper().getRoleDAO();

Создание запросов

Для построения специфичных запросов можно создавать свои методы в классе DAO.

public List<Goal> getGoalByName(String name)  throws SQLException{
  QueryBuilder<Goal, String> queryBuilder = queryBuilder();
  queryBuilder.where().eq(Goal.GOAL_NAME_FIELD_NAME, "First goal");
  PreparedQuery<Goal> preparedQuery = queryBuilder.prepare();
  List<Goal> goalList =query(preparedQuery);
  return goalList;
}

Как видно, здесь производился поиск по объектам с соответствующим полем имени.
При построении более сложных запросов помимо eq (что означает equals) есть и другие вроде gt(greater), ge(greater and equals) и остальных соответсвующих стандартным where-конструкцииям в SQL (полный список здесь)
Для построения сложного запроса можно добавлять and:
queryBuilder.where().eq(Goal.GOAL_NAME_FIELD_NAME, "First goal").and().eq(Goal.GOAL_NOTES_NAME_FIELD_NAME,”aaa”);

Соответсвенно помимо запросов из БД подобным образом можно удалять и обновлять таблицы.

Работа с вложенным сущностями

Рассмотрим отношения вида много к одному. Допустим объект Goal имеет поле указывающее на Role.
Соответственно в классе Goal поле должно быть аннотированно

@DatabaseField(foreign = true)
   private Role role;
   public void setRole(Role value){

      this.role = value;
   }

   public Role getRole(){
      return role;
   }

Теперь мы можем сохранить наши объекты в БД

   Goal g = new Goal();
   g.setName(“asd”);
   Role r = new Role();
   g.setRole(r);
   HelperFactory.getHelper.getRoleDAO().create(r);
   HelperFactory.getHelper.getGoalDAO().create(g);

Чтобы получить доступ к объекту класса Role, который связан с объектом типа Goal:

   Goal g = HelperFactory.getHelper.getRoleDAO().getGoalByName(“asd”);
   Role r = g.getRole();
   HelperFactory.getHelper.getRolelDAO().refresh(g);

refresh() необходимо выполнять, чтобы r получил все поля из БД соответвующие этому объекту.

При хранинии ссылки на коллекцию подход немного отличается:

в классе Goal:

   @ForeignCollectionField(eager = true)
      private Collection<Role> roleList;

   public addRole(Role value){
      value.setGoal(this);
      HelperFactory.GetHelper().getRoleDAO().create(value);
      roleList.add(value);
   }

   public void removeRole(Role value){
      roleList.remove(value);
      HelperFactory.GetHelper().getRoleDAO().delete(value);
   }

Аргумент в аннотации eager означает, что все объекты из roleList будут получаться из БД вместе с извлечением объекта типа Goal. Обязательной является ссылка на Goal в объекте Role. А так же нужно отдельно сохранять с помощью RoleDAO каждый из объектов Role.
Соответственно в классе Role должна быть аннотация:

   @DatabaseField(foreign = true, foreignAutoRefresh = true)
   private Goal goal;

Если не ставить eager=true, то будет осуществлена lazy-инициализация, т.е. при запросе объекта Goal объекты соотвествующие коллекции roleList не будут извлечены. Для их извлечения нужно будет произвести их итерацию:

   Iterator<Role> iter = state.goal.getRoleList().iterator();
   while (iter.hasNext()) {
      Role r = iter.next();
   }

Ссылка на библиотеку.
Ссылка на туториалы.

Статья подготовлена при поддержке SurfStudio

Автор: nekdenis

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js