На данный момент для платформы 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