На данный момент для платформы Android существует несколько решений, позволяющих реализовать ORM-подход для работы с базой данных, но основных два. Это ORMLite и GreenDAO.
Для новичков стоит сделать отступление и рассказать что такое ORM вообще. ORM — object-ralational mapping. Объектно-реляционное отображение означает, что программисту гораздо удобнее оперировать с объектами, которые он использует в своём приложении, нежели чем с таблицами, в которых хранятся данные в реляционных базах данных. Для связи реляционных СУБД с объектной моделью приложения используются ORM-технологии. Так для решения задач объектно-реляционного отображения в Android используют один из сторонних фреймворков. GreenDAO и ORMLite — являются библиотеками с открытым кодом.
Фреймворк от немецких разработчиков, который они используют в своих приложениях.
Не использует отражения (Reflection) для работы
Максимальная производительность для Android ORM
Минимальное использование памяти
Маленький размер библиотеки, не оказывает большого влияния на Android проект.
Популярный фреймворк для Java, имеющий адаптированную версию под Android.
… хм… я их не находил) всё что кажется не возможным, реализуется при более глубоком изучении доков.
OrmLite я использую в нескольких и проектах и знаком с ним достаточно тесно. Поэтому рассажу как раз о нем.
Каждый класс, отображение в базу данных (сохрание в БД) которого мы хотим сделать должен быть аннотирован. (можно использовать class-configuration, но это совсем другая история)Стоит заметить, что у класса должен обязательно присутствовать конструктор без аргументов. Пример аннотации:
Первая аннотация
Перед каждым полем должна быть аннотация
Первичным ключем может служить любое поле — достаточно указать у него
Для индексации столбца в БД указывается
Полное описание всех доступных агументов аннотации полей можно посмотреть здесь
Также помимо аннотаций ORMLite можно использовать привычные некоторым javax.persistence аннотации
Существует несколько способов получения доступа к данным БД с помощью ORMLite в андроид. Можно наследовать каждую activity от ORMLiteBaseActivity и тогда нам не придется следить за жизненным циклом соединения с БД, но при этом мы не сможем получить доступа из других классов. (см примеры)
Гораздо предпочтительнее подход, рассмотренный далее.
Нужно создать класс, который будет инстанцировать помощника в создании и работе с БД:
Обращение к нему будет происходить во время начала и конца жизни приложения:
Это предотвратит утечки памяти из-за незакрытого соединения с БД
Так же не забываем описать наш класс наследник Application в манифесте:
Создадим класс DataBaseHelper, который будет отвечать за создание БД и за получение ссылок на DAO:
Как видно в примере, нужно описать создание полей и таблицы, а также реализовать получение ссылки на синглтон с DAO-объектом. Теперь рассмотрим непосредственно сами DAO.
Самой простой реализацией является следующий класс
Здесь создан лишь один дополнительный метод для получения коллекции всех объектов класса Role.
Основные методы create, update, delete реализованы в предке BaseDaoImpl.
Для обращения к методом класса RoleDao в любом классе приложения достаточно обратиться к классу-фабрике:
Для построения специфичных запросов можно создавать свои методы в классе DAO.
Как видно, здесь производился поиск по объектам с соответствующим полем имени.
При построении более сложных запросов помимо
Для построения сложного запроса можно добавлять
Соответсвенно помимо запросов из БД подобным образом можно удалять и обновлять таблицы.
Рассмотрим отношения вида много к одному. Допустим объект Goal имеет поле указывающее на Role.
Соответственно в классе Goal поле должно быть аннотированно
Теперь мы можем сохранить наши объекты в БД
Чтобы получить доступ к объекту класса Role, который связан с объектом типа Goal:
При хранинии ссылки на коллекцию подход немного отличается:
в классе Goal:
Аргумент в аннотации
Соответственно в классе Role должна быть аннотация:
Если не ставить
Ссылка на библиотеку.
Ссылка на туториалы.
Для новичков стоит сделать отступление и рассказать что такое 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();
}
Ссылка на библиотеку.
Ссылка на туториалы.