ORMLite - простая работа с базой данных

ORMLite - простая работа с базой данных


Дата публикации 27.10.2016



На данный момент существует множество фреймворков, позволяющих организовать работу с базами данных из java-приложений. В этой статье я расскажу про легковесный и в то же время функциональный фреймворк, реализующий технологию ORM - ORMLite.

Для начала давайте разберёмся что такое 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. Зато я использую другие параметры:

  1. Параметр id = true указывает на то что данное поле является первичным ключём в таблице. Например в AssetType это поле id.
  2. Параметр unique = true я задал для поля name, так как хочу чтобы значение в нём было уникальным (классификатор же :) ).
  3. Параметр 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