Для начала давайте разберёмся что такое ORM и с чем его едят. ORM расшифровывается как Object-Relational Mapping или по русски - объектно-реляционное отображение. Эта такая технология, которая позволяет работать с базой данных через объекты языка программирования, при этом отпадает необходимость писать SQL-запросы, что очень удобно.
Ранее я уже писал про очень крутой и популярный ORM фреймворк - Hibernate. Если Hibernate используется в крупных проектах и позволяет решать сложные задачи, то ORMLite напротив, для этого совершенно не предназначен. Зато ORMLite легковесный и простой в использовании, поэтому используется в небольших проектах, где нужно организовать простую работу с базой данных, не утяжеляя само приложение. Наибольшую популярность OrmLite получил в разработке приложений под Android.
В качестве примера я сделал простое веб-приложение в котором использую OrmLiteдля работы с базой данных SQLite: https://github.com/AlexeyKutepov/equity-calculator. Это примитивный калькулятор для подсчёта собственного капитала. Подробнее об этом приложении я расскажу в другой статье, а сейчас затрону только то что касается непосредственно ORMLite.
Для того чтобы начать использовать OrmLite, нужно добавить соответствующие зависимости в файл pom.xml:
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>${sqlite.version}</version>
</dependency>
<dependency>
<groupId>com.j256.ormlite</groupId>
<artifactId>ormlite-jdbc</artifactId>
<version>${ormlite.version}</version>
</dependency>
В моём случае это драйвер для работы с базой данных SQLite - sqlite-jdbc и библиотека ormlite-jdbc (ORMLite). Версии этих зависимостей у меня вынесены в блок <properties>:
<ormlite.version>5.0</ormlite.version>
<sqlite.version>3.8.11.2</sqlite.version>
Затем нужно настроить подключение к базе данных в конфиге Spring:
<bean id="databaseUrl" class="java.lang.String">
<constructor-arg index="0" value="jdbc:sqlite:equity.db" />
</bean>
<bean id="connectionSource" class="com.j256.ormlite.jdbc.JdbcConnectionSource" init-method="initialize">
<property name="url" ref="databaseUrl" />
</bean>
Первый бин databaseUrl является обычной строкой подключения к базе данных и идёт как параметр для инициализации бина connectionSource. Второй бин connectionSource это JDBC драйвер, который обеспечивает подключение к базе данных. Он скоро нам понадобится для инициализации DAO, но об этом чуть позже.
Теперь нужно создать классы-сущности, которые будут описывать структуру самих таблиц в базе данных. Например для моего приложения понадобится 4 таблицы: Asset (список активов), AssetType (классификатор активов), Liability (список обязательств) и LiabilityType (классификатор обязательств). Вот как это выглядит на диаграмме:
Структура таблиц очень простая, но есть один момент: таблица Asset связана с таблицей AssetType по типу "многие к одному" и такая же связь у таблиц Liability и LiabilityType. Теперь это всё нужно описать в классах-сущностях.
Сначала создадим класс-сущность для таблицы AssetType:
package ru.kutepov.db.entity;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
/**
* Категория актива
*/
@DatabaseTable(tableName = "AssetType")
public class AssetType {
@DatabaseField(id = true)
private int id;
@DatabaseField(unique = true, canBeNull = false)
private String name;
public AssetType() {
}
public AssetType(int id, String name) {
this.id = id;
this.name = name;
}
public AssetType(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Класс AssetType имеет аннотацию @DatabaseTable(tableName = "AssetType"), которая указывает что данный класс будет ассоциирован с таблицей, имя которой задано в параметре tableName. В данном случае класс и таблица в базе данных имеют одинаковое имя - AssetType, но имена могут быть разными, тут уже на ваше усмотрение. Кроме того, согласно документации по OrmLite, каждый класс-сущность должен иметь конструктор без аргументов, поэтому я добавил пустой конструктор класса.
В AssetType определены 2 переменные:
- id - переменная типа int с аннотацией @DatabaseField(id = true)
- name - переменная типа String c аннотацией @DatabaseField(unique = true, canBeNull = false)
Аннотация @DatabaseField указывает что переменная будет по умолчанию ассоциирована с одноимённым полем в таблице AssetType. Если вы хотите чтобы переменная была ассоциирована с полем у которого другое название, то нужно задать параметр columnName для аннотации @DatabaseField. Например, если у нас есть переменная name, которую нужно ассоциировать с полем Name, то необходимо задать такую аннотацию для переменной: @DatabaseField(columnName = "Name")
В моём случае названия переменных совпадают с названием полей в базе данных и я не задаю параметр columnName. Зато я использую другие параметры:
- Параметр id = true указывает на то что данное поле является первичным ключём в таблице. Например в AssetType это поле id.
- Параметр unique = true я задал для поля name, так как хочу чтобы значение в нём было уникальным (классификатор же :) ).
- Параметр canBeNull = false тоже задан для поля name, так как я хочу чтобы оно не было пустым ни при каких обстоятельствах.
Ещё очень важно не забыть создать методы get и set для каждой переменной в классе-сущности. Для многих разработчиков это очевидно, но лучше на всякий случай напомню.
Далее создаём сущность для таблицы Asset:
package ru.kutepov.db.entity;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
/**
* Актив
*/
@DatabaseTable(tableName = "Asset")
public class Asset {
@DatabaseField(generatedId = true)
private int id;
@DatabaseField(canBeNull = false)
private String name;
@DatabaseField(canBeNull = false)
private double value;
@DatabaseField(foreign = true, canBeNull = false, foreignAutoRefresh = true)
private AssetType assetType;
public Asset() {
}
public Asset(String name, double value, AssetType assetType) {
this.name = name;
this.value = value;
this.assetType = assetType;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getValue() {
return value;
}
public void setValue(double value) {
this.value = value;
}
public AssetType getAssetType() {
return assetType;
}
public void setAssetType(AssetType assetType) {
this.assetType = assetType;
}
}
Тут всё сделано по аналогии с предыдущим примером. Нововведения касаются только 2-х переменных в этом классе:
- id - переменная типа int с аннотацией @DatabaseField(generatedId = true)
- assetType - переменная типа AssetType c аннотацией @DatabaseField(foreign = true, canBeNull = false, foreignAutoRefresh = true)
Как и в предыдущем примере поле id, с которым ассоциирована переменная id, будет выступать в качестве первичного ключа. Отличие в параметре generatedId = true, который указывает на то, что это поле будет заполняться автоматически при сохранении нового объекта в базу данных.
Через переменную assetType я связал таблицу Asset с таблицей AssetType по принципу "многие к одному". Для этого я указал в качестве типа assetType ранее созданную сущность AssetType и задал параметр foreign = true в аннотации @DatabaseField. Сама переменная по умолчанию будет ассоциирована с полем assetType_id в базе данных SQLite и в этом поле будет храниться идентификатор записи из таблицы AssetType.
По умолчанию OrmLite обеспечивает ленивую загрузку данных, а это значит что данные из связанных таблиц не будут загружены в целях экономии памяти. Так как AssetType это классификатор, то мне было бы удобно чтоб класс AssetTypeполностью выгружался вместе в с классом Asset и для этого я задал параметр foreignAutoRefresh = true. Данный параметр нужно использовать очень осторожно, так как он может значительно увеличить объём потребляемой памяти. Так же при работе с foreignAutoRefresh нужно помнить про ещё один важный параметр - maxForeignAutoRefreshLevel. Параметр maxForeignAutoRefreshLevel указывает количество уровней загрузки данных из связанных таблиц и по умолчанию равен 2. Это значит что при получении объекта Asset так же будет получен связанный объект AssetType, и если бы у AssetType были бы связи с другими объектами, то эти объекты тоже бы подгрузились в память. Этот момент тоже нужно учитывать, так как эта ещё одна возможность просадить память при неправильном подходе.
Аналогично создаём сущность LiabilityType:
package ru.kutepov.db.entity;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
/**
* Категория обязательства
*/
@DatabaseTable(tableName = "LiabilityType")
public class LiabilityType {
@DatabaseField(id = true)
private int id;
@DatabaseField(unique = true, canBeNull = false)
private String name;
public LiabilityType() {
}
public LiabilityType(int id, String name) {
this.id = id;
this.name = name;
}
public LiabilityType(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Ну и конечно же Liability:
package ru.kutepov.db.entity;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
/**
* Обязательства
*/
@DatabaseTable(tableName = "Liability")
public class Liability {
@DatabaseField(generatedId = true)
private int id;
@DatabaseField(canBeNull = false)
private String name;
@DatabaseField(canBeNull = false)
private double value;
@DatabaseField(foreign = true, canBeNull = false, foreignAutoRefresh = true)
private LiabilityType liabilityType;
public Liability() {
}
public Liability(String name, double value, LiabilityType liabilityType) {
this.name = name;
this.value = value;
this.liabilityType = liabilityType;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getValue() {
return value;
}
public void setValue(double value) {
this.value = value;
}
public LiabilityType getLiabilityType() {
return liabilityType;
}
public void setLiabilityType(LiabilityType liabilityType) {
this.liabilityType = liabilityType;
}
}
Завершающим этапом при работе с ORMLite это создание и конфигурирование DAO-классов. Для тех кто не в курсе, DAO расшифровывается как data access object, и является объектом, который предоставляет абстрактный интерфейс к какому-либо типу базы данных или механизму хранения. С помощью DAO-классов мы можем выполнять любые манипуляции над записями в таблицах БД, например создавать, удалять, выполнять поиск по заданным параметрам и т.д.
Мне очень нравится как построена работа с DAO в ORMLite. Например вы можете проинициализировать в коде стандартный DAO-класс и пользоваться уже готовыми методами:
Dao<AssetType, Integer> assetTypeDao = DaoManager.createDao(connectionSource, AssetType.class);
А можно отнаследоваться от класса BaseDaoImpl и расширить DAO новыми методами, которые нужны для полноценной работы приложения:
package ru.kutepov.db.dao;
import com.j256.ormlite.dao.BaseDaoImpl;
import com.j256.ormlite.support.ConnectionSource;
import ru.kutepov.db.entity.Asset;
import ru.kutepov.db.entity.AssetType;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AssetDAO extends BaseDaoImpl<Asset, Integer> {
public AssetDAO(ConnectionSource connectionSource, Class<Asset> dataClass) throws SQLException {
super(connectionSource, dataClass);
}
/**
* Получить список активов определённой категории
* @param assetType категория актива
* @return {@link List<Asset>}
*/
public List<Asset> getAssetListByAssetType(AssetType assetType) throws SQLException {
Map<String, Object> map = new HashMap<>();
map.put("assetType_id", assetType.getId());
return this.queryForFieldValues(map);
}
}
В примере выше я создал DAO для работы с таблицей Asset и расширил его методом getAssetListByAssetType, который возвращает список активов по типу из таблицы AssetType. Класс BaseDaoImpl является обобщённым и при наследовании от него нужно задать 2 типа: тип сущности для которой мы делаем DAO и тип первичного ключа. В моём случае это Asset для сущности и Integer для первичного ключа таблицы Asset (поле id): BaseDaoImpl<Asset, Integer>.
В AssetDAO есть конструктор, который имеет 2 параметра: connectionSource(соединение с базой данных) и dataClass (в него будет передаваться значение Asset.class).
Для того чтобы использовать AssetDAO, его нужно где-то проинициализировать. Я предпочитаю инициализировать DAO в конфиге спринга:
<bean id="assetDAO" class="ru.kutepov.db.dao.AssetDAO">
<constructor-arg index="0" ref="connectionSource"/>
<constructor-arg index="1" value="ru.kutepov.db.entity.Asset"/>
</bean>
Аналогично можно создать DAO для работы с таблицами AssetType, Liability и LiabilityType. Готовый пример вы можете посмотреть тут: https://github.com/AlexeyKutepov/equity-calculator