Показаны сообщения с ярлыком JPA. Показать все сообщения
Показаны сообщения с ярлыком JPA. Показать все сообщения

среда, 6 апреля 2016 г.

Разработка приложений со Spring Boot. Часть 2: Работа с базами данных

В своём предыдущем посте я описал начало разработки приложения с использованем Spring Boot. В этом посте я расскажу о работе с реляционными базами данных.

Spring Boot и реляционные БД

Для работы с реляционным базами данных в Spring Boot есть стартер spring-boot-starter-data-jpa:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Из названия следует, что стандартный способ работы с реляционными базами данных реализован посредством Spring Data JPA. Основываясь на своём опыте, могу сказать, что возможности этого фреймворка и JPA полностью покрывают потребности практически любого проекта. Исключения составляют сложные запросы с вложенными запросами и кучей джоинов, но и они решаются при помощи хранимых процедур и представлений.
Теперь нашему проекту нужно добавить связь с базой данных. Для демонстрационного проекта вполне хватит и локальной БД вроде HSQLDB или H2:
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

В application.properties нужно добавить два свойства, что бы схема базы данных создавалась при запуске приложения:
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop

Теперь всё готово для написания классов-сущностей и логики работы с базой данных.

Классы сущностей и репозитории

В простейшем хелпдеске есть как минимум две сущности: заявка с описанием проблемы и её обсуждение в виде комментариев.
Для заявки создадим класс Ticket:
@Entity
public class Ticket implements Serializable {

    private static final long serialVersionUID = 20160403085401L;

    /**
     * Идентификатор, генерируется автоматически при создании новой записи.
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * Краткое описание проблемы.
     */
    private String issue;

    /**
     * Подробное описание проблемы.
     */
    @Column(columnDefinition = "TEXT")
    private String description;

    /**
     * Дата создания заявки.
     */
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date createDate;

    /**
     * Дата закрытия заявки.
     */
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date resolveDate;

    // getters, setters, hashCode, equals, toString
}


Для комментария к заявке создадим класс TicketComment:
@Entity
public class TicketComment implements Serializable {

    private static final long serialVersionUID = 20160403085801L;

    /**
     * Идентификатор комментария. Генерируется автоматически при создании нового комментария.
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * Комментарий.
     */
    private String comment;

    /**
     * Дата создания комментария
     */
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date createDate;

    /**
     * Заявка, к которой привязан комментарий.
     */
    @ManyToOne
    private Ticket ticket;

    // getters, setters, hashCode, equals, toString
} 

При помощи JPA-аннотаций можно более подробно сконфигурировать классы сущностей и их маппинг в таблицы базы данных.

Теперь нам нужно определить два интерфейса для взаимодействия с базой данных. Для работы с заявками это будет TicketRepository:

@Repository
public interface TicketRepository extends CrudRepository<Ticket, Long> {

    /**
     * Поиск всех незакрытых заявок.
     */
    List<Ticket> findByResolveDateIsNull(Sort sort);

    /**
     * Получение полного списка заявок с возможностью сортировки.
     */
    List<Ticket> findAll(Sort sort);

    /**
     * Закрытие заявки.
     */
    @Transactional
    @Modifying
    @Query("update Ticket set resolveDate = now() where id = :id")
    int resolveTicket(@Param("id") long id);

    /**
     * Повторное открытие заявки.
     */
    @Transactional
    @Modifying
    @Query("update Ticket set resolveDate = null where id = :id")
    int reopenTicket(@Param("id") long id);
}

Рассмотрим код описанного интерфейса более подробно:
  • Аннотация @Repository указывает, что данный класс или интерфейс является репозиторием и объект этого класса будет использоваться при внедрении зависимостей.
  • Описанный интерфейс расширяет CrudRepository, в котором уже определены основные CRUD-операции. Так же можно расширить интерфейс JpaRepository. Дженерики указывают, что данный репозиторий работает с сущностью Ticket, а классом первичных ключей во всех операциях является Long.
  • Метод findByResolveDateIsNull производит поиск всех записей сущности Ticket, у которых свойство resolveDate не задано.
  • Методы resolveTicket и reopenTicket обновляют запись, делая заявку закрытой или открытой. Для методов, которые модифицируют данные в БД необходимы аннотации @Transactional и @Modifying.
Реализацию данного интерфейса писать не нужно, за нас это сделает Spring Data JPA. Но если со стандартными методами, а так же с методами, с аннотацией @Query всё понятно, то как фреймворк определит поведение метода findByResolveDateIsNull? Всё просто, поведение метода реализуется на основании его названия:
  • Начало названия метода определяет, что нужно сделать в запросе. Например, findBy вернёт записи, соотвествующие критерии выборки (SELECT), deleteBy - удалит (DELETE).
  • Дальше указываются условия запроса. В приведённом примере условием является ResolveDateIsNull, что будет транслировано в resolve_date is null (на SQL). Если бы мы хотели просто произвести поиск по полному соответствию resolveDate, то сигнатура метода выглядела бы следующим образом: List<Ticket> findByResolveDate(Date date). Условия выборки можно объединять при помощи And или Or в зависимости от ситуации.
Методам репозитория так же могут передаваться объекты классов, определяющих дополнительные манипуляции при поиске. В указанном выше методе таким объектом является sort, добавляющий возможность изменения сортировки получаемых записей.
Метод, возвращающий несколько записей из БД может возвращать Iterable, Collection или даже Stream, здесь каждый решит за себя, что ему будет удобнее использовать.

Возможности Spring Data JPA серьёзно ускоряют и упрощают процесс разработки, позволяя меньше отвлекаться на написание кода работы с БД и сконцентрироваться на написании бизнес-логики. Но, что если запрос к БД имеет больше 2-3 условий, или условия выборки несколько сложнее? В этом случае удобным будет использование аннотации @Query, которая позволяет описывать запросы в JPQL или SQL (при установленном свойстве native). Рассмотрим на примере репозитория комментариев, TicketCommentRepository:

@Repository
public interface TicketCommentRepository extends CrudRepository<TicketComment, Long> {

    List<TicketComment> findByTicket(Ticket ticket);

    @Query(value = "select tc from TicketComment tc join ticket t where t.id = :id")
    List<TicketComment> findByTicketId(@Param("ticketId") long id);
}

В свойстве value мы описали простой JPQL-запрос, в котором производится выборка всех TicketComment, у которых id вложенного объекта ticket соответствует искомому. Имена методов, у которых есть аннотация @Query, могут иметь свободную форму, Spring Data JPA реализует поведение этих методом на основании запроса.

По сути, оба описанных выше метода имеют одну и ту же функциональность - они возвращают список комментариев к указанной заявке.

На этом этапе у нас есть классы сущностей и репозитории с реализацией основных действий с БД. В следующем посте я опишу работу со Spring WebMVC на примере контроллеров и представлений с использованием Thymeleaf.

А что если...

  • Нужна нормальная СУБД, а не H2? 
    • Добавляем в зависимости нужный драйвер:
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

    • Описываем свойства подключения в application.properties:
spring.datasource.platform=postgresql
spring.datasource.url=jdbc:postgresql://localhost:5432/postgresql
spring.datasource.username=postgresql
spring.datasource.password=postgresql
spring.datasource.driver-class-name=org.postgresql.Driver

  • Нужна нестандартная реализация источника данных?
    • Добавляем в зависимости библиотеку с желаемой реализацией источника данных:
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>

    • В классе конфигурации создаём метод с аннотацией @Bean, в котором создаём объект источника данных:
    @Value("${spring.datasource.type}")
    private String dataSourceClassName;

    @Value("${spring.datasource.url}")
    private String dataSourceUrl;

    @Value("${spring.datasource.username}")
    private String dataSourceUsername;

    @Value("${spring.datasource.password}")
    private String dataSourcePassword;

    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDataSourceClassName(dataSourceClassName);
        dataSource.setDataSourceProperties(getDataSourceProperties());

        return dataSource;
    }

    private Properties getDataSourceProperties() {
        Properties properties = new Properties();
        properties.setProperty("url", dataSourceUrl);
        properties.setProperty("user", dataSourceUsername);
        properties.setProperty("password", dataSourcePassword);

        return properties;
    }

четверг, 30 июля 2015 г.

Spring Framework, Hibernate, JPA: Аннотации или XML?

Споры на данную тему в стане приверженцев Spring Framework существуют с момента релиза третьей версии данного фреймворка. И хотя в версии 2.5 уже можно было частично заменить конфигурирование приложения аннотациями, полностью отказаться от XML было нереально.

Оба подхода имеют свои плюсы и минусы, и на мой взгляд, если нет возможости выбрать какой-то конкретный, то логично использовать их вместе.

Стоит отметить забавный момент, что среди разработчиков фреймворка во время Spring Framework 3 не было единого мнения по этому поводу - в книге Pro Spring 3 использовались XML-конфигурации, а в книге Pro Spring MVC: with Web Flow - аннотации.

XML

С XML-конфигурациями в Spring Framework я работаю ещё с версии 2.5. На тот момент альтернатив не было. Зато можно было хотя бы использовать аннотацию @Autowired для внедрения зависимостей, чему я предпочитал непосредственное указание зависимостей в XML через сеттеры и конструкторы. С тех пор я унаследовал привычку конфигурировать практически всё при помощи XML, в том числе JPA и Hibernate. Для меня главным аргументом в пользу XML была возможность изменять настройки приложения на лету без необходимости собирать проект заново. Такая необходимость возникала, но нечасто. Хотя объективности ради стоит заметить, что именяющиеся компоненты Spring-приложения вполне можно конфигурировать properties-файлами или через JNDI.

Против XML главным аргументом всегда являлся тот факт, что в больших проектах XML-конфигурацию очень сложно читать ввиду большого количества конфигурируемых компонентов и нередко большого количества файлов конфигурации. В качестве примера можно привести проекты CMDBuild и OpenOLAT.

Актуальность XML-конфигураций теряется ещё и в том случае, когда разработчик не имеет физического доступа к развёрнутому приложению, как это зачастую бывает в серьёзных организациях. Именно это и стало для меня решающим фактором в пользу перехода на использование аннотаций.


Аннотации

В пользу аннотаций можно однозначно записать простоту. Конфигурирование приложения посредством аннотаций происходит проще, чем при использовании XML. Всё делается тем же Java-кодом. Разработчику в данном случае даже не обязательно знать XML, хотя я сильно сомневаюсь, что в природе существуют Java-разработчики без базовых знаний XML.

Отдельно стоит отметить тот факт, что в мире Java EE всё давно конфигурируется при помощи аннтоаций: EJB, CDI, JPA и т.д. и Spring Framework совместим со всеми аннотациями Java EE.

Ну а минус, соответственно, заключается в отсутствии возможности конфигурировать проект на лету.

Groovy

Отдельно стоит упомянуть возможность конфигурировать Spring-приложения при помощи Groovy-классов. Если классы конфигурации не компилировать, а оставлять скриптами, то приложение можно будет конфигурировать на лету. И при этом сам процесс конфигурирования будет значительно более гибким, чем даже при использовании XML. Но это если в проекте используется Groovy.

Вывод

Вывод прост - нужно стремиться к использованию аннотаций, раз это действительно проще, правильнее и рекомендуется разработчиками Spring Framework. Тем более, что в Java EE это стандарт, а на горизонте маячит Spring Boot, хотя никто не запрещает и там использовать XML.

Однако в плане маппингов JPA и Hibernate я так и остался при мнении, что использование XML значительно удобнее аннотаций, особенно в случаях, когда POJO-классы находятся в отдельной от бизнес-логики библиотеке. В этом случае клиентским Java-приложениям при использовании тех же API не нужно будет тянуть ненужные зависимости.