среда, 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;
    }

Комментариев нет:

Отправить комментарий