2 июня 2014 г.

Hibernate, Spring и maven

Актуальная статья на эту тему доступна на моём новом сайте devmark.ru

Давайте создадим maven-проект, который бы работал с базой при помощи Hibernate. А для уменьшения связности между классами будем использовать dependency injection в лице Spring. В качестве СУБД давайте использовать PostgreSQL.

Конкретные пошаговые инструкции ниже будут приведены для Idea 13, но вы можете использовать вашу любимую среду разработки, благо универсальная структура maven-проекта позволяет нам отвязаться от конкретной IDE.

Проверялось на следующей конфигурации: Hibernate 4.3, Spring 4.0, maven 3, postgresql 9.1.

Добавление необходимых зависимостей


Итак, File - New Project. Там выбираем Maven Project, в правой панели убедитесь, что снята галка Create from archetype, ибо для наглядности будем создавать простой проект без архетипа. Жмём Next. Откроется второе окно, GroupId указываем любое, например developer-remarks. В качестве ArtifactId указываем, например, hibernate-entities. Жмём далее, указываем любое имя проекта, но лучше указать такое же, как и ArtifactId. Жмём Finish.

Среда разработки создаст нам заготовку для будущего проекта. В том числе в корне вы обнаружите файл pom.xml. Откройте его и добавьте секцию projects/properties:

<properties>
<org.springframework.version>4.0.5.RELEASE</org.springframework.version>
<org.hibernate.version>4.3.5.Final</org.hibernate.version>
</properties>

Эта секция может содержать любые параметры нашего проекта. В данном случае здесь содержатся номера версий для Spring и Hibernate. Вы можете указать более свежие.

Также следует добавить секцию project/dependencies. В неё добавьте следующие зависимости для поддержки Hibernate:

<!--Hibernate-->
<dependency>
    <groupId>org.hibernate.common</groupId>
    <artifactId>hibernate-commons-annotations</artifactId>
    <version>4.0.4.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>${org.hibernate.version}</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>${org.hibernate.version}</version>
</dependency>

Для поддержки Spring:

<!--Spring Framework-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${org.springframework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>${org.springframework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${org.springframework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>${org.springframework.version}</version>
</dependency>

Драйвер PostgreSQL:

<!--PostgreSQL-->
<dependency>
    <groupId>postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>9.1-901.jdbc4</version>
</dependency>

Если вы хотите использовать другую СУБД, можете указать другой драйвер, но тогда не забудьте внести соответствующие изменения в параметры подключения (будут рассмотрены далее).

Плюс добавим вспомогательные зависимости и логирование:

<!--Commons-->
<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-pool</groupId>
    <artifactId>commons-pool</artifactId>
    <version>1.6</version>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

<!--Other Libs-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>15.0</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

На этом с зависимостями закончим. Их нам будет вполне достаточно. Теперь перейдём к написанию кода.

Создание сущностей


Начнём с классов-сущностей. Это классы, поля которых соответствуют значениям полей таблицы в БД. Благодаря Hibernate мы будем оперировать этими классами как более высоким уровнем абстракции, вместо того, чтобы напрямую писать SQL-запросы. Класс-сущность отличает аннотация javax.persistence.Entity.

Давайте создадим класс Content, у которого будут поля id и title. Здесь и далее я опускаю get- и set-методы полей классов-сущностей для компактности. Добавьте их самостоятельно (Alt + Insert, затем выберите Getter and Setter).

@Entity
public class Content {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "header", length = 100, nullable = false)
    private String title;
}

id - это служебное поле, которое в таблице будет представлено первичным ключом (аннотация @Id). Очень удобно, когда это поле автоматическое увеличивает своё значение на 1 при добавлении новой строки в таблицу. За такое поведение отвечает аннотация @GeneratedValue. Её необязательный параметр strategy указывает Hibernate на способ формирования уникального значения. В нашем случае в PostgreSQL будем создано поле типа serial (поле, автоматически увеличивающее значение при вставке новой строки, что нам и нужно). Также можно использовать последовательность (sequence), если ваша СУБД это поддерживает (просто выберите GenerationType.SEQUENCE). Тогда потребуется указать дополнительные параметры вроде имени последовательности.

title - просто текстовое поле. В нашем примере на него повесим аннотацию @Column. Делать это не обязательно и нужно только в том случае, когда мы хотим явно указать имя этой колонки в таблице или другие параметры. В нашем примере колонка title в таблице будет отображаться в поле title нашего класса. Также оно будет иметь ограничение на длину в 100 символов и для него будут запрещены неопределённые значения (not null в описании таблицы). Если бы мы не указывали всех этих параметров, Hibernate использовал бы значения по умолчанию (title отражалось бы в title, имело длину в 255 символов и для него были бы разрешены неопределённые значения).

С одной стороны Hibernate позволяет контролировать определения полей в таблице, а с другой стороны позволяет избегать этих действий во избежание излишнего нагромождения аннотаций. Если ваша бизнес-логика не налагает специальных условий на уровень базы данных, то лучше отказаться от лишних аннотаций. Это улучшит поддержку кода.

Помимо таких простых сущностей, Hibernate позволяет сохранять в БД целые иерархии классов. Давайте добавим двоих наследников нашего класса: книгу со свойством "количество страниц" и музыкальный трек с полем "битрейт" (количество бит, используемых для кодирования звука).

@Entity
public class Book extends Content {
    private int pageCount;
}

@Entity(name = "music_track")
public class Music extends Content {
    private int bitRate;
}

Во втором классе при помощи @Entity мы принудительно задаём имя таблицы как music_track.

В базовый класс следует добавить модификатор abstract и навесить на него ещё две аннотации:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "class_type", length = 50)
public abstract class Content { ... }

Аннотация @Inheritance определяет собственно способ хранения нашей иерархии в базе данных. В данном случае все классы будут храниться в одной таблице, поля которой будут множеством полей всех классов, участвующих в иерархии. Каждая строка таблицы будет представлять собой экземпляр конкретного класса. Тип записи будет определяться при помощи служебного поля class_type длиной 50 символов (см. аннотацию @DiscriminatorColumn). В это поле таблицы будет записываться сокращённое имя класса. Параметры аннотации @DiscriminatorColumn являются опциональными, как и в случае с @Column. На основании этой служебной колонки Hibernate будет инициализировать экземпляр соответствующего класса. Но что будет с полями, которые не относятся к данному классу, а используются в других наследниках? А ничего - они будут заполнены неопределёнными значениями в таблице.

Помимо стратегии хранения всей иерархии классов в одной таблице, также можно хранить их в разных (InheritanceType.TABLE_PER_CLASS). В нашем случае получится две таблицы. В этом случае общие поля будут созданы в каждой таблице, что приводит к дублированию полей.

Наиболее правильный вариант с точки зрения нормализации данных - это хранить общие поля в одной таблице (все поля из Content в нашем случае), а для полей из наследников создать свои таблицы (всего тогда у нас получится три таблицы). Отвечает за это значение InheritanceType.JOINED. Но такой вариант наиболее затратен в плане ресурсов, ибо при каждой манипуляции с данными потребуется объединять сразу несколько таблиц.

Как именно хранить данные в БД выбирать вам исходя из ваших потребностей. И Hibernate здесь хорош тем, что позволяет настраивать это одним параметром.

А теперь давайте добавим в нашу иерархию автора контента. Поскольку он есть и у музыкального трека, и у книги, то добавить его следует в базовый класс. Но у автора есть имя, фамилия и отчество. Очевидно, эти параметры связаны между собой. Давайте создадим отдельный класс, чтобы сгруппировать связанные поля:

@Embeddable
public class UserInfo {
    private String firstName;
    private String middleName;
    private String lastName;
}

Аннотация @Embeddable позволяет встраивать данную сущность в любую другую. В целевой таблице они будут представлять такие же поля, как и "родные" поля родительской сущности. Встраивание в родительскую сущность Content следует выполнить так:

    @Embedded
    private UserInfo author = new UserInfo();

Встраиваемые сущности также наследуются, поэтому в сущности Book у нас будет теперь на три поля больше. Обращаться к ним нужно будет примерно так (не забывайте создавать геттеры и сеттеры в каждой сущности):

new Book().getAuthor().getFirstName()

В итоге у нас должна получиться такая иерархия классов-сущностей:


(Если что, построить такую диаграмму можно в Idea, выбрав в контекстном меню на нужном классе Diagrams - Show Diagram)

Настройка Spring


Теперь давайте создадим конфигурационный файл с настройками Spring. Назовите файл как угодно, например, beans.xml и поместите его в ту же директорию src/main/resources/META-INF. Путь до этого файла нам нужно будет указать один раз при старте программы.

Пример файла настроек Spring:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

Сначала идёт перечисление всех xml-схем, которые используются в этом файле. Их наличие обязательно.

    <context:annotation-config/>

Означает, что конфигурация spring-бинов будет выполняться на основе аннотаций. В предыдущих версия Spring конфигурация была возможно только при помощи xml, что превращало конфигурирование в рутинный процесс.

    <context:component-scan base-package="developer.remarks"/>

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

Далее идёт создание цепочки бинов (что очень характерно для конфигурирования в формате xml). Имя бина указывается атрибутом id. Бин является экземпляром того класса, который указан в атрибуте class. По умолчанию Spring везде подставляет один и тот же экземпляр, но это можно менять настройками или аннотациями.

<bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.postgresql.Driver"/>
    <property name="url" value="jdbc:postgresql://localhost:5432/testdb"/>
    <property name="username" value="test"/>
    <property name="password" value="123456"/>
</bean>

Здесь мы конфигурируем источник данных (data source), в свойствах которого указываем основные параметры подключения (адрес, имя, пароль, драйвер и т.п.)

Следующие два бина содержат информацию о той СУБД, к которой мы подключаемся. Также здесь указывается класс диалекта в hibernate, который учитывает особенности нашей базы.

<bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>

<bean id="jpaVendorAdapter"
      class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
    <property name="database" value="POSTGRESQL"/>
    <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQL9Dialect"/>
</bean>

Далее идёт самый основной бин менеджера сущностей. Именно он будет подставляться в наш сервис. Здесь мы указываем местонахождение файла persistence.xml (о нём см. ниже), имя persistenceUnit, а также ссылаемся на уже определённые нам другие бины.

<bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
      id="entityManagerFactory">
    <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/>
    <property name="persistenceUnitName" value="developer.remarks.persistence.unit"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>
    <property name="jpaDialect" ref="jpaDialect"/>
</bean>

Ну и наконец менеджер транзакций. Наши действия с БД будут автоматически выполняться в рамках транзакций при помощи данного бина.

<bean id="transactionManager"
      class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaDialect" ref="jpaDialect"/>
</bean>

</beans>

На этом конфигурация Spring завершена.

Persistence-unit


Спецификация JPA, частичной реализацией которой является Hibernate, требует наличия файла persistence.xml. Обычно в нём хранится вся информация о подключении к БД, но т.к. мы уже указали её в другом файле, то здесь укажем лишь то, что требуется по стандарту. Создайте файл persistence.xml в каталоге src/main/resources/META-INF. Пример содержимого этого файла:

<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="developer.remarks.persistence.unit">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
        </properties>
    </persistence-unit>
</persistence>

persistence-unit - это имя ваших настроек. Само имя может быть произвольным, только оно должно совпадать с тем, что написано в настройках Spring.

hibernate.hbm2ddl.auto в значении create. Это означает, что Hibernate при старте приложения создаст необходимые таблицы в соответствии с описанием классов-сущностей. То есть наше приложение можно будет запускать на совсем пустой БД. Если таблицы с такими именами уже были в базе до этого, они будут удалены. Поэтому в промышленных системах так делать не следует, ибо вы рискуете потерять данные между запусками приложения.

В качестве альтернативы можно использовать значение update (при изменении структура таблиц будет обновлена) или validate (структура таблиц не меняется, а лишь проверяется). В тестовых целях можно использовать значение create-drop (таблицы будут удалены сразу по завершении сессии) - это удобно, если не хотите оставлять после себя мусор в БД.

Манипуляция сущностями при помощи EntityManager


Для работы с БД при помощи Spring нам нужно создать два класса: один для предоставления основных операций с данными (добавление, изменение, удаление, выборка), а другой - для более высокоуровневой логики сохранения (на случай, если мы захотим сохранять не в БД, а в другое хранилище данных). Spring оперирует интерфейсами: он ищет подходящую реализацию и подставляет её экземпляр в нужное поле (это и есть внедрение зависимостей или dependency injection), поэтому для каждого сервиса нам нужно создать по интерфейсу.


MediaDao - интерфейс сервиса работы с БД. Dao расшифровывается как data access object. Dao предоставляет абстрактный интерфейс к хранилищу данных, а его реализация будет содержать в себе менеджер сущностей. MediaService - интерфейс высокоуровневого сервиса, который содержит в себе ссылку на MediaDao.

Интерфейс MediaDao:

public interface MediaDao {
    public void save(Content content);
    public List<Content> getAll();
}

Метод save может сохранять любой тип записей (любой тип медианосителей из нашей библиотеки, music_track или book), а метод getAll - возвращает список всех сохранённых элементов из БД.

Его реализация MediaDaoImpl:

package developer.remarks.dao;

import developer.remarks.models.Content;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;

@Repository
public class MediaDaoImpl implements MediaDao {

    @PersistenceContext
    private EntityManager em;

    @Override
    public void save(Content content) {
        em.persist(content);
    }

    @Override
    public List<Content> getAll() {
        return em.createQuery("from Content", Content.class).getResultList();
    }
}

Аннотация @Repository указывает на то, что данный класс является Spring-бином. Именно эту аннотацию следует использовать для классов, обеспечивающих сохранение данных. Даже несмотря на то, что программа выполнится без ошибок, если вы укажете вместо @Repository аннотацию @Service (предназначена для сервисов), @Controller (предназначена для Spring MVC) и даже @Component (более общая аннотация относительно всех перечисленных), всё равно используйте @Repository для Dao. Тем самым вы получите дополнительный функционал для своего бина, вроде специальной обработки исключений при сохранении данных.

Аннотация @PersistenceContext позвляет легко и просто проинициализировать менеджера сущностей. Его настройку мы уже прозвели ранее в двух конфигурационных файлах. Теперь сохранение в БД возможно вызовом одного метода persist.

Обратите внимание на то, как производится выборка в методе getAll. Здесь представлен самый простой jpa-запрос, который выбирает все записи из таблицы. Но мы можем ограничивать выборку подобно тому, как это делается в обычном SQL. Единственным отличием будет то, что здесь мы оперируем не полями в таблице, а полями класса-сущности.

Пример jpa-запроса для выборки записи с определённым заголовком (title):

em.createQuery("from Content c where c.title = :title", Content.class)
.setParameter("title", "Moby - Lift Me Up")
.getResultList();

Запросы всегда нужно типизировать, чтобы результирующий список всегда был типизирован конкретным классом. Целевой класс указывается в качестве второго параметра метода createQuery (Content.class).

Конкретное значение в условии фильрации мы указываем в виде параметра (двоеточие и произвольное имя). Далее значение параметра устанавливается отдельным методом setParameter, где указывается имя целевого параметра и его значение. Таким образом мы исключаем возможность взлома при помощи sql-инъекций.

Также обратите внимание, что не обязательно указывать select, если мы выбираем класс целиком. Если же нам нужно одно из его полей, то нужно использовать select, а также типизировать запрос типом данных этого поля.

Перейдём к более высокоуровневому сервису MediaService. Его интерфейс:

public interface MediaService {

    /**
     * Сохраняет элемент в библиотеке
     * @param content Элемент библиотеки (книга или аудиозапись)
     */
    public void save(Content content);

    /**
     * Возвращает все элементы, о которых есть данные в библиотеке
     * @return Список элементов библиотеки
     */
    public List<Content> getAll();
}

Как видите, он полностью аналогичен интерфейсу dao. Реализация:

package developer.remarks.services;

import developer.remarks.dao.MediaDao;
import developer.remarks.models.Content;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service("storageService")
public class MediaServiceImpl implements MediaService {

    @Autowired
    private MediaDao dao;

    @Transactional
    @Override
    public void save(Content content) {
        dao.save(content);
    }

    @Override
    public List<Content> getAll() {
        return dao.getAll();
    }
}

Здесь мы как раз используем аннотацию @Service, поскольку в данном случае у нас не происходит работа с БД. Также в скобках мы принудительно указываем имя бина. Это делать совершенно не обязательно и приведено здесь только в качестве примера. По умолчанию Spring сгенерит имя бина как camel-case вариант имени интерфейса.

Аннотация @Autowired как раз и реализует механизм подстановки подходящей реализации для указанного интерфейса. Поле dao будет проинициализировано экземпляром класса MediaDaoImpl, который будет создан в одном экземпляре для всех компонентов.

Важно отметить, что на методах, модифицирующих данные (добавление, изменение, удаление) должна стоять аннотация @Transactional, чтобы Spring автоматически начинал транзакцию перед вызовом метода и отправлял изменения в хранилище данных по окончанию работы метода. То есть вам не нужно принудительно делать commit. Однако если не повесить @Transactional на метод, то ваши изменения никак не отразятся на БД.

Собираем всё воедино


Наконец привожу пример класса со статическим методом main, в котором будет вызываться наш сервис и будут сохраняться тестовые данные.

public class Program {

    private static final Logger logger = Logger.getLogger(Program.class);

    public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("META-INF/beans.xml");
MediaService service = (MediaService) context.getBean("storageService");
service.save(getBook());
service.save(getTrack());
logger.info("Список всех элементов библиотеки мультимедиа:");
for (Content content : service.getAll()) {
logger.info(content);
}
    }

    private static Content getBook() {
        Book book = new Book();
        book.setTitle("Над пропастью во ржи");
        book.getAuthor().setFirstName("Джером");
        book.getAuthor().setMiddleName("Дэвид");
        book.getAuthor().setLastName("Сэлинджер");
        book.setPageCount(500);
        return book;
    }

    private static Content getTrack() {
        Music track = new Music();
        track.setTitle("Moby - Lift Me Up");
        track.getAuthor().setFirstName("Ричард");
        track.getAuthor().setMiddleName("Мэлвилл");
        track.getAuthor().setLastName("Холл");
        track.setBitRate(256);
        return track;
    }

}

Программа просто добавляет две записи в БД: одну книгу и один музыкальный трек. Затем отображает список всех сохранённых записей.

Обратите внимание на метод main. Здесь мы создаём ApplicationContext, для которого указываем расположение нашего файла с конфигурацией Spring. Расположение можно настраивать. Затем получаем ссылку на экземпляр сервиса MediaService, обратившись к нему по имени из текущего контекста. Это имя мы явно прописали в предыдущем разделе.

Чтобы у нас была возможность запускать jar-файл из командной строки вне всякого окружения, сконфигурируем maven-shade-plugin, который помимо обычного jar при сборке сформирует также и runnable jar. В настройках плагина вам потребуется указать точку входа (main-класс), а также специальные обработчики (AppendingTransformer) для двух служебных файлов (spring.handlers и spring.schemas), которые дублируются в нескольких библиотеках из зависимостей. Если их специальным образом не обработать, то произойдёт ошибка при загрузке Spring.

Также при необходимости можно настроить логирование. Как это делается при помощи log4j, подробно рассмотрено здесь. Исходники проекта доступны на моей страничке в github.

В целом же хочется отметить, что связка Hibernate + Spring + Maven сейчас используется во многих Enterprise-приложениях. Освоив работу с этими инструментами и фреймворками, вы станете довольно востребованным специалистом на рынке труда. Данная связка является довольно гибкой и настриваемой, где каждый уровень абстракции является независимым и взаимозаменяемым.

Если Вам помог материал моей статьи, пожалуйста, нажмите кнопку +1 (рекомендовать в Google). Спасибо!