Русский
Русский
English
Статистика
Реклама

Spring data

Перевод Что нового в Spring Data (Klara Dan von) Neumann

20.08.2020 16:15:21 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Разработчик на Spring Framework.
Подробнее о курсе можно узнать посмотрев запись дня открытых дверей.



Spring Data Neumann это первый релиз, после перехода на новый шестимесячный релизный цикл. Сокращение сроков между релизами позволит нам чаще выпускать новые фичи, а это, в свою очередь, ускорит и вас. В этом релизе, помимо нового функционала, есть также важные изменения, потенциально ломающие совместимость с предыдущими версиями.

Изменение мажорных версий


Для проектов, указанных ниже, увеличен номер мажорной версии из-за изменений, ломающих совместимость в публичных API или драйверах:

  • Spring Data JDBC 2.0 (предыдущая версия 1.1)
  • Миграция с 1.1 на 2.0 описана в этом посте.
  • Spring Data MongoDB 3.0 (предыдущая версия 2.2)
  • Spring Data для Apache Cassandra 3.0 (предыдущая версия 2.2)
  • Spring Data Couchbase 4.0 (предыдущая версия 3.2)
  • Spring Data Elasticsearch 4.0 (предыдущая версия 3.2)
  • Подробнее об изменениях см. этом посте.


Перед тем как перейти к описанию новой функциональности, давайте посмотрим на изменения в API. Подробнее об этом смотрите в разделах по обновлению (Upgrading) в документации соответствующих модулей.

Если вы не готовы обновляться сейчас, то имейте в виду, что предыдущий релиз Moore будет поддерживаться еще в течение двенадцати месяцев.

JDBC


У каждого SQL-хранилища есть свои особенности, требующие особого подхода. Для улучшения их поддержки были внесены изменения, которые повлияли на увеличение мажорной версии. Теперь AbstractJdbcConfiguration по умолчанию пытается определить Dialect базы данных из заданного DataSource или зарегистрированного DialectResolver. По умолчанию модуль JDBC поставляется с диалектами для H2, HSQLDB, MySQL, Postgres, MariaDB, Microsoft SqlServer и DB2. Spring Data JDBC теперь по умолчанию экранирует все имена таблиц и колонок. Несмотря на то что из-за этого вам, возможно, придется изменить ваши CREATE TABLE или аннотации @Column, это даст большую гибкость при именовании объектов.

MongoDB


Единый jar с драйверами для MongoDB (mongo-java-driver) разбит на несколько: -sync и -reactivestreams, что позволяет вам выбрать только необходимый драйвер. То есть, и синхронный, и реактивный драйверы MongoDB теперь являются необязательными зависимостями, которые необходимо добавлять вручную. При переходе на новые драйвера некоторые из уже устаревших API были окончательно удалены, что повлияло на классы конфигурации, такие как AbstractMongoConfiguration и пространства имен XML, предоставляемые Spring Data. Подробнее смотрите раздел по обновлению в документации.

Apache Cassandra


Давно назревшее обновление драйверов Apache Cassandra до 4.0 не только обновляет пакет и структуру данных, но также изменяет поведение в кластере и в обработке сеансов. Это привело к серьезным изменениям в конфигурации, которые влияют на XML-конфигурацию и могут повлиять на конфигурацию в коде для каких-то сложных сценариев (сложнее, чем простая настройка по умолчанию AbstractCassandraConfiguration).

Couchbase


Вслед за Couchbase SDK мы обновились с версии 3.x до 4.x, что добавило автоматическое управление индексами и поддержку транзакций. Подробнее читайте в блоге Couchbase.

Elasticsearch


Добавлена поддержка HTTP Client API, SSL и Proxy. Также сделан ряд изменений, включающих в себя оптимизацию и удаление устаревшего API, что повлияло на изменение мажорного номера версии. В модуль Elasticsearch теперь входит Document, включающий в себя Get-, Index- и Search-Requests, что позволяет при маппинге использовать такие типы как SearchHit, SearchHits и SearchPage.

Теперь давайте перейдем к новшествам.

Репозитории с поддержкой корутин Kotlin


Релиз Neumann продолжает развитие поддержки корутин Kotlin, начавшуюся в предыдущем релизе Moore, добавив их поддержку в репозиториях.

Корутины поддерживаются через реактивные Spring Data-репозитории. Теперь можно использовать реактивные методы запросов или писать свои suspended-функции.

interface StudentRepository : CoroutineCrudRepository<Student, String> {    suspend fun findOne(id: String): User    fun findByLastname(firstname: String): Flow<Student>}


@Primary-репозитории и ключевое слово search


Эти два небольших изменения улучшают поиск бинов репозиториев и именование методов запросов. Теперь аннотация @Primary на репозиториях-интерфейсах учитывается в конфигурации бинов, что помогает контейнеру резолвить зависимости. Для методов запросов теперь можно использовать префикс "search", аналогично "find". То есть теперь можно писать методы "search...By...", например, searchByFirstname.

Несмотря на то что это было сделано для таких баз данных, как Elasticsearch, давайте двигаться дальше и посмотрим как search...By... можно использовать в Spring Data R2DBC.

Генерация запросов R2DBC


До настоящего времени в Spring Data R2DBC для методов запросов использовалась аннотация @Query за исключением методов по умолчанию, предоставляемых через интерфейсы *.Repository. Теперь генерация запросов по имени метода работает аналогично другим модулям:

interface StudentRepository extends ReactiveCrudRepository<Student, Long> {Flux<Student> searchByLastname(String lastname); (1)}


Это эквивалентно:

@Query("select id, firstname, lastname from customer c where c.lastname = :lastname")

Разбивка на страницы и генерация запросов для JDBC


Spring Data JDBC 2.0 поддерживает еще больше реляционных баз данных. Теперь мы запускаем наши интеграционные тесты на H2, HSQLDB, MySQL, MariaDB, PostgreSQL и DB2.

Для этих баз данных мы поддерживаем генерацию запросов и разбивку на страницы. Например:

interface StudentRepository extends PagingAndSortingRepository<Student, Long> {Page<Student> findByLastname(String lastname);}


В этом релизе мы также продолжаем двигаться в сторону NoSQL, начав с MongoDB и нового способа модификации документов.

MongoDB Update Aggregations


Это важное изменение (которое не было полностью готово в релизе Moore) позволяет использовать Aggregation Pipeline для обновления данных. Таким образом, изменения могут содержать сложные выражения, такие как условия по значениям полей, например:

AggregationUpdate update = Aggregation.newUpdate()    .set("average").toValue(ArithmeticOperators.valueOf("tests").avg())    .set("grade").toValue(ConditionalOperators.switchCases(        when(valueOf("average").greaterThanEqualToValue(90)).then("A"),        when(valueOf("average").greaterThanEqualToValue(80)).then("B"),        when(valueOf("average").greaterThanEqualToValue(70)).then("C"),        when(valueOf("average").greaterThanEqualToValue(60)).then("D"))        .defaultTo("F")    );template.update(Student.class)    .apply(update)    .all();


Также Spring Data MongoDB, несомненно, выиграет от недавно добавленной в другие модули поддержки встроенных объектов (embedded object).

Поддержка embedded-типов в Apache Cassandra


Apache Cassandra теперь поддерживает маппинг встроенных типов (embedded type), которые уже давно были доступны в Spring Data JDBC. В модели предметной области встроенные объекты используются для объектов-значений (Value Object), свойства которых хранятся в одной таблице. В следующем примере над полем Student.name стоит аннотация @Embedded, что приводит к тому, что все поля класса Name будут храниться в таблице Student, состоящей из трех столбцов (student_id, firstname и lastname):

public class Student {    @PrimaryKey("student_id")    private String studentId;    @Embedded(onEmpty = USE_NULL)    Name name;}public class Name {    private String firstname;    private String lastname;}


Аудит в Elasticsearch


Поскольку в ElasticSearch наличие id не является достаточным критерием для определения того, является ли объект новым, необходимо при реализации Persistable предоставить дополнительную информацию с помощью метода isNew():

@Document(indexName = "person")public class Person implements Persistable<Long> {    @Id private Long id;    private String lastName;    private String firstName;    @Field(type = Date)    private Instant createdDate;    private String createdBy    @Field(type = Date)    private Instant lastModifiedDate;    private String lastModifiedBy;    @Override    public boolean isNew() {        return id == null || (createdDate == null && createdBy == null);    }}


После этого добавление @EnableElasticsearchAuditing в конфигурацию регистрирует все компоненты, необходимые для аудита.

Neo4j


Spring Data Neo4j теперь поддерживает синтаксис запросов Neo4j 4.0 с параметрами. Синтаксис с заполнителями (placeholder) был ранее объявлен устаревшим, а сейчас полностью удален. Теперь модуль зависит от последних версий драйверов Neo4j-OGM и Neo4j Java для улучшения взаимодействия с последней версией Neo4j.
Также идет активная работа по поддержке реактивности для графовых баз данных и ее интеграция в Spring Data с Neo4j RX (хотя это и не входит в текущий релиз, но уже готово для включения в следующий).

Apache Geode / VMware Tanzu GemFire


Модули Spring Data для Apache Geode и VMware Tanzu GemFire (spring-data-geode и spring-data-gemfire) объединены в один проект под общим названием SDG. Apache Geode был обновлен до 1.12.0, а GemFire до 9.10.0, который, в свою очередь, основан на Apache Geode 1.12. Кроме того, SDG компилируется и запускается на версиях JDK с 8 по 14.

SDG теперь поддерживает публикацию событий автотранзакций, которая преобразует событие Cache TransactionEvent от GemFire / Geode Cache в соответствующий ApplicationEvent контекста.

Также теперь можно приостановить отправку событий на AEQ, настроенном с помощью SDG. Кроме того, при создании приложений на основе GemFire / Geode Locator с использованием аннотации SDG @LocatorApplication можно настроить Locator для подключения к другим Locator, создавая таким образом высокодоступный и устойчивый кластер.



Узнать подробно о курсе.




Читать ещё:


Подробнее..

Перевод Введение в Spring Data JDBC

04.12.2020 18:08:20 | Автор: admin

Для будущих студентов курса "Java Developer. Professional" подготовили перевод полезного материала.

Также приглашаем принять участие в открытом уроке на тему
"Введение в Spring Data jdbc"


Spring Data JDBC был анонсирован в 2018 году. Целью было предоставить разработчикам более простую альтернативу JPA, продолжая при этом следовать принципам Spring Data. Подробнее узнать о мотивах, лежащих в основе проекта, вы можете в документации.

В этой статье я покажу несколько примеров использования Spring Data JDBC. Здесь не будет подробного руководства, но, надеюсь, приведенной информации хватит, чтобы попробовать его самостоятельно. Очень хорошо, если вы уже знакомы со Spring Data JPA. Исходный код вы можете найти в github.

Для быстрого старта я использовал этот шаблон.

Предварительная подготовка

Из зависимостей нам нужны data-jdbc стартер, flyway для управления схемой и драйвер postgres для подключения к базе данных.

// build.gradledependencies {    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'    implementation 'org.flywaydb:flyway-core'    runtimeOnly 'org.postgresql:postgresql'}

Далее настраиваем приложение для подключения к базе данных:

# application.ymlspring:  application:    name: template-app  datasource:    url: jdbc:postgresql://localhost:5432/demo_app?currentSchema=app    username: app_user    password: change_me    driver-class-name: org.postgresql.Driver

Маппинг сущностей

Для этого примера будем использовать следующую таблицу:

create table book (    id varchar(32) not null,    title varchar(255) not null,    author varchar(255),    isbn varchar(15),    published_date date,    page_count integer,    primary key (id));

И соответствующий java-класс (обратите внимание, что @Id импортируется из org.springframework.data.annotation.Id):

// Book.javapublic class Book {    @Id    private String id;    private String title;    private String author;    private String isbn;    private Instant publishedDate;    private Integer pageCount;}

Однако, если мы запустим тест:

// BookRepositoryTest.java@Testvoid canSaveBook() {    var book = Book.builder().author("Steven Erikson").title("Gardens of the Moon").build();    var savedBook = bookRepository.save(book);    assertThat(savedBook.getId()).isNotBlank();    assertThat(savedBook.getAuthor()).isEqualTo(book.getAuthor());    assertThat(savedBook.getTitle()).isEqualTo(book.getTitle());    assertThat(savedBook).isEqualTo(bookRepository.findById(savedBook.getId()).get());}

То увидим ошибку ERROR: null value in column "id" violates not-null constraint. Это происходит, потому что мы не определили ни способ генерации id ни значение по умолчанию. Поведение Spring Data JDBC в части идентификаторов немного отличается от Spring Data JPA. В нашем примере нужно определить ApplicationListener для BeforeSaveEvent:

// PersistenceConfig.java@Beanpublic ApplicationListener<BeforeSaveEvent> idGenerator() {    return event -> {        var entity = event.getEntity();        if (entity instanceof Book) {            ((Book) entity).setId(UUID.randomUUID().toString());        }    };}

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

Методы запросов

Одной из особенностей проектов Spring Data является возможность определять методы запросов в репозиториях. Spring Data JDBC использует здесь несколько иной подход. Для демонстрации определим метод запроса в BookRepository:

Optional<Book> findByTitle(String title);

И если запустим соответствующий тест:

@Testvoid canFindBookByTitle() {    var title = "Gardens of the Moon";    var book = Book.builder().author("Steven Erikson").title(title).build();    var savedBook = bookRepository.save(book);    assertThat(bookRepository.findByTitle(title).get()).isEqualTo(savedBook);}

Получим ошибку Caused by: java.lang.IllegalStateException: No query specified on findByTitle. В настоящее время Spring Data JDBC поддерживает только явные запросы, задаваемые через @Query. Напишем sql-запрос для нашего метода:

@Query("select * from Book b where b.title = :title")Optional<Book> findByTitle(@Param("title") String title);

Тест пройден! Не забывайте об этом при создании репозиториев.

Примечание переводчика: в Spring Data JDBC 2.0 появилась поддержка генерации запросов по именам методов.

Связи

Для работы со связями Spring Data JDBC также использует другой подход. Основное отличие в том, что отсутствует ленивая загрузка. Поэтому если вам не нужна связь в сущности, то просто не добавляйте ее туда. Такой подход основан на одной из концепций предметно-ориентированного проектирования (Domain Driven Design), согласно которой сущности, которые мы загружаем, являются корнями агрегатов, поэтому проектировать надо так, чтобы корни агрегатов тянули за собой загрузку других классов.

Один-к-одному

Для связей "один-к-одному" и "один-ко-многим" используется аннотация @MappedCollection. Сначала посмотрим на "один-к-одному". КлассUserAccount будет ссылаться на Address. Вот соответствующий sql:

create table address(    id      varchar(36) not null,    city    varchar(255),    state   varchar(255),    street  varchar(255),    zipcode varchar(255),    primary key (id));create table user_account(    id         varchar(36)  not null,    name       varchar(255) not null,    email      varchar(255) not null,    address_id varchar(36),    primary key (id),    constraint fk_user_account_address_id foreign key (address_id) references address (id));

Класс UserAccount выглядит примерно так:

// UserAccount.javapublic class UserAccount implements GeneratedId {    // ...other fields    @MappedCollection(idColumn = "id")    private Address address;}

Здесь опущены другие поля, чтобы показать маппинг address. Значение в idColumn это имя поля идентификатора класса Address. Обратите внимание, что в классе Address нет ссылки на класс UserAccount, поскольку агрегатом является UserAccount. Это продемонстрировано в тесте:

//UserAccountRepositoryTest.java@Testvoid canSaveUserWithAddress() {    var address = stubAddress();    var newUser = stubUser(address);    var savedUser = userAccountRepository.save(newUser);    assertThat(savedUser.getId()).isNotBlank();    assertThat(savedUser.getAddress().getId()).isNotBlank();    var foundUser = userAccountRepository.findById(savedUser.getId()).orElseThrow(IllegalStateException::new);    var foundAddress = addressRepository.findById(foundUser.getAddress().getId()).orElseThrow(IllegalStateException::new);    assertThat(foundUser).isEqualTo(savedUser);    assertThat(foundAddress).isEqualTo(savedUser.getAddress());}

Один-ко-многим

Вот sql, который будем использовать для демонстрации связи "один-ко-многим":

create table warehouse(    id       varchar(36) not null,    location varchar(255),    primary key (id));create table inventory_item(    id        varchar(36) not null,    name      varchar(255),    count     integer,    warehouse varchar(36),    primary key (id),    constraint fk_inventory_item_warehouse_id foreign key (warehouse) references warehouse (id));

В этом примере на складе (warehouse) есть много товаров/объектов (inventoryitems). Поэтому в классе Warehouse мы также будем использовать @MappedCollection для InventoryItem:

public class Warehouse {    // ...other fields    @MappedCollection    Set<InventoryItem> inventoryItems = new HashSet<>();    public void addInventoryItem(InventoryItem inventoryItem) {        var itemWithId = inventoryItem.toBuilder().id(UUID.randomUUID().toString()).build();        this.inventoryItems.add(itemWithId);    }}public class InventoryItem {    @Id    private String id;    private String name;    private int count;}

В этом примере мы устанавливаем поле id во вспомогательном методе addInventoryItem. Можно также определить ApplicationListener для класса Warehouse с обработкой BeforeSaveEvent, в котором установить поле id для всех InventoryItem. Вам не обязательно делать в точности так, как сделано у меня. Посмотрите тесты с демонстрацией некоторых особенностей поведения связи "один-ко-многим". Главное то, что сохранение или удаление экземпляра Warehouse влияет на соответствующие InventoryItem.

В нашем случае InventoryItem не должен знать о Warehouse. Таким образом, у этого класса есть только те поля, которые описывают его. В JPA принято делать двусторонние связи, но это может быть громоздким и провоцировать ошибки, если вы забудете поддерживать обе стороны связи. Spring Data JDBC способствует созданию только необходимых вам связей, поэтому обратная связь "многие-к-одному" здесь не используется.

Многие-к-одному и многие-ко-многим

В рамках этого руководства я не буду вдаваться в подробности о связях "многие-к-одному" или "многие ко многим". Я советую избегать связей "многие-ко-многим" и использовать их только в крайнем случае. Хотя иногда они могут быть неизбежны. Оба этих типа связей реализуются в Spring Data JDBC через ссылки на Id связанных сущностей. Поэтому имейте ввиду, что здесь вам предстоит еще немного потрудиться.

Заключение

Если вы использовали Spring Data JPA, то большая часть из того, что я рассказал, должна быть вам знакома. Я уже упоминал ранее, что Spring Data JDBC стремится быть проще, и поэтому отсутствует ленивая загрузка. Помимо этого, отсутствует кеширование, отслеживание "грязных" объектов (dirty tracking) и сессии (session). Если в Spring Data JDBC вы загружаете объект, то он загружается полностью (включая связи) и сохраняется тогда, когда вы сохраняете его в репозиторий. Примеры, которые я показал, очень похожи на свои аналоги в JPA, но помните, что многие концепции Spring Data JPA отсутствуют в Spring Data JDBC.

В целом мне нравится Spring Data JDBC. Признаю, что это может быть не лучший выбор для всех приложений, однако я бы рекомендовал его попробовать. Как человек, который в прошлом боролся с ленивой загрузкой и dirty tracking, я ценю его простоту. Я думаю, что это хороший выбор для простых предметных областей, которые не требуют большого количества нестандартных запросов.

На этом пока все, спасибо за чтение! Надеюсь, вы нашли это руководство полезным и оно будет отправной точкой для использования Spring Data JDBC.


Подробнее о курсе "Java Developer. Professional".


Записаться на открытый урок "Введение в Spring Data jdbc".

Подробнее..

JPA Buddy Умный помощник половина работы

17.03.2021 14:12:14 | Автор: admin

От переводчика: это статья моего коллеги @aleksey-stukalov, которую мы опубликовали в блоге JPA Buddy пару месяцев назад. С тех пор мы выпустили JPA Buddy 2.0, но все сказанное в этой статье актуальности не потеряло.

Ну что ж, Hello World... После почти года разработки наконец-то вышла первая версия JPA Buddy! Это инструмент, который должен стать вашим верным помощником по написанию кода для проектов с JPA и всем, что с этим связано: Hibernate, Spring Data, Liquibase и другим ПО из типичного стека разработки.

Чем он вам поможет? Если кратко, JPA Buddy упростит работу с JPA и тем самым сэкономит ваше время. В этой статье мы взглянем на основные фичи JPA Buddy, немного обсудим его историю и поговорим о его преимуществах. Надеюсь, он займет достойное место среди любимых инструментов Java-разработчиков, которые пользуютсяJPA, Spring, Liquibase и, конечно же, самой продвинутой Java IDE IntelliJ IDEA.

Откуда растут ноги

Мы создатели CUBA Platform (кстати, не так давно мы переименовали ее в Jmix :)) среды быстрой разработки приложений на Java. Платформа CUBA уникальный продукт. Он состоит из двух частей: фреймворка и инструмента разработки CUBA Studio. Одной из самых полюбившихся частей CUBA Studio стал Entity Designer. В сообществе CUBA более 20 000 разработчиков, и почти все они отмечают, насколько легко и быстро он дает создавать модель данных. Даже для тех, кто никогда не слышал о JPA, такие вещи как создание JPA-сущностей, ограничений модели данных и DDL-скриптов становятся легкой задачей.

В 2016 году CUBA стала open-source проектом. С того момента мы приняли участие в десятках конференций по всему миру, собрали много отзывов и лучше поняли, что именно нужно разработчикам. Они нас часто спрашивали: Можно ли использовать ваш конструктор сущностей без CUBA Platform?. Рады сообщить, что с появлением JPA Buddy мы теперь можем смело ответить да!.

JPA Buddy не требует дополнительных зависимостей это просто инструмент. Вы можете пользоваться им не только на новых, но и на уже существующих проектах, которые используют JPA. Это плагин для IntelliJ IDEA, который помогает вам писать связанный с JPA код, находить и исправлять потенциальные ошибки, проводить рефакторинг и выполнять другие трудоемкие, но достаточно шаблонные операции.

Область применения

Перед тем, как написать первую строчку исходного кода JPA Buddy, мы провели опрос и собрали различные сценарии использования JPA и сопутствующих технологий. Результат оказался достаточно предсказуемым: среднестатистическое приложение в настоящее время это приложение на Spring Boot с Hibernate в качестве реализации ORM, Spring Data JPA в качестве механизма управления данными и Flyway или Liquibase для системы миграции базы данных. Ах да, чуть не забыли про Lombok... В общем, на этом стеке мы и сконцентрировались в первом релизе.

Говоря о предназначении JPA Buddy, мы поставили перед собой следующие цели:

  • Сократить написание шаблонного кода вручную: инструмент должен генерировать код быстрее ручного ввода

  • Не заставлять тратить время на чтение документации: инструмент должен предоставлять интуитивно понятные визуальные конструкторы

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

  • Выявлять потенциальные проблемы и предлагать решения для наиболее распространенных: инструмент должен предупреждать разработчика о проблемах на этапе написания кода, а не в рантайме

  • Обеспечить обзор проекта с точки зрения данных и удобную навигацию между связанными сущностями

В первом релизе мы смогли реализовать достаточно значительный объем функциональности, охватывающей большинство аспектов разработки модели данных. Хорошая новость для тех, кто использует Liquibase: инструменты для работы с ним уже реализованы и доступны. Не очень хорошая новость для пользователей Flyway: он входит в список наиболее приоритетных фич :)

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

  • Аннотации Hibernate: @Where, @NaturalId, @Formula, поисковые аннотации и т. п.

  • Визуальный конструктор запросов

  • Аудит с использованием Envers и Spring Data JPA

  • Генерация модели по существующей схеме базы данных

  • Поддержка Kotlin

В дальнейшем мы учтем и другие функции: поддержка Quarkus и Micronaut, REST API и генерация пользовательского интерфейса для CRUD-операций.

Как видите, впереди еще долгий путь, и мы были бы очень признательны за вашу помощь в расстановке приоритетов разработки не стесняйтесь высказывать свою точку зрения через нашу форму, Twitter или Discord.

Общий обзор

Давайте посмотрим, как выглядит JPA Buddy. После его установки у вас появятся 3 новые панели инструментов: JPA Structure, JPA Palette и JPA Inspector.

JPA Structure

Панель JPA Structure расположена в левом нижнем углу. Она позволяет посмотреть на проект с точки зрения модели данных. С ее помощью можно:

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

  2. Создавать объекты, связанные с данными: сущности, JPA-конвертеры/Hibernate-типы, Spring Data репозитории и Liquibase-скрипты.

  3. Увидеть, для каких сущностей созданы какие репозитории.

  4. Просматривать скрипты Liquibase и их внутреннюю структуру.

  5. Настраивать параметры плагина, такие как соединение с БД, Persistence Units и некоторые другие, которые плагин не обнаружил автоматически.

JPA Palette и JPA Inspector

Панель JPA Palette находится справа вверху, и ее содержимое зависит от контекста. Она доступна только тогда, когда Buddy готов предложить чтото для быстрой генерации кода. Сейчас панель появляется при редактировании следующих объектов: JPA Entity, Spring Data репозиториев и Liquibase-скриптов. Для генерации какого-либо элемента просто выберите нужный вариант в списке и кликните по нему дважды.

JPA Inspector размещается справа внизу, под JPA Palette, и открывается вместе с ним. С помощью JPAPalette можно генерировать новый код, а с помощью JPAInspector редактировать уже существующий. В нем видно, как можно сконфигурировать выбранную часть кода, будь то поле сущности или выражение изLiquibase-скрипта.

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

Интеграция с Liquibase

Хорошие новости для тех, кто использует Liquibase для управления версиями схемы базы данных: JPA Buddy тесно с ним интегрирован, он позволяет удобно редактировать и создавать Liquibase-скрипты, а также гибко управлять типами атрибутов. Давайте посмотрим, как именно JPA Buddy поможет вам работать с Liquibase.

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

Во-вторых, JPA Buddy дает определить свои собственные маппинги для сопоставления Java-типов (Конвертеров/Hibernateтипов) и типов, которые должны использоваться для каждой конкретной СУБД. Приведем несколько примеров, когда эта функция будет полезна:

  • Допустим, у вас в сущности есть поле типа byte[]. Что генератор схемы Hibernate, что Liquibase сопоставят ваш массив с довольно экзотическим типом OID. Думаю, многие разработчики предпочли бы использовать bytea вместо предлагаемого по умолчанию типа. Это можно легко сделать, создав маппинг с Java-типа byte[] на БД-тип bytea в настройках проекта в JPA Buddy.

  • Использование JPA-конвертера или кастомного Hibernate-типа вызывает непредсказуемое поведение или даже ошибки в стандартных генераторе схемы Hibernate и генераторе скриптов Liquibase. С помощью JPA Buddy можно явно указать нужный тип, чтобы решить эту проблему.

  • Поля типа String по-умолчанию хранятся как varchar, то есть не поддерживают Юникод. С помощью JPA Buddy легко изменить этот тип на nvarchar, с которым этой проблемы нет.

Все эти случаи обычно решаются с помощью атрибута columnDefinition, однако это решение не будет работать для проектов, где одновременно используется несколько СУБД. JPA Buddy позволяет указать, какой именно маппинг использовать для конкретной СУБД.

Наконец, функция, экономящая наибольшее количество времени наш генератор Liquibase-скриптов. Исходя из нашего опыта (который совпадает с опытом разработчиков принявших участие в ранее упомянутом опросе), есть два основных способа создания Liquibase-скриптов:

  • Написание вручную

  • Создание скриптов путем сравнения двух баз данных: исходной (представляющей фактическое состояние модели) и целевой (с предыдущим состоянием модели)

Для тех, кто предпочитает первый вариант, JPA Buddy включает уже упомянутый ранее конструктор Liquibase-скриптов.

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

Начнем с небольшой проблемы: может случиться так, что база данных, используемая для разработки на ноутбуке разработчика, не того же типа, что и база данных на продакшене. Это можно исправить, например, запустив MS SQL в Docker на Mac.

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

ВJPA Buddy есть замечательная функция генерации Liquibase-скриптов напрямую, путем сравнения ваших объектов JPA сцелевой базой данных (или snapshotом целевой базы данных). Выможете использовать H2в целях разработки ипо-прежнему генерировать правильные журналы изменений для Oracle, MSSQL или того, что выиспользуете впродакшене. Подобный подход гарантирует, что если увас есть мусор вжурналах изменений, это именно тот мусор, который есть увас висходном коде. Все, что вам нужно, это содержать модель данных вчистоте, итогда миграция неприведет кнежелательным артефактам вбазе данных продакшена.

Еще одна особенность генератора журнала изменений это возможность фильтровать полученные выражения по 3 категориям:

  • содержащие только безопасные операторы, которые не могут дать сбой при обновлении или полностью уронить ваше приложение, например, добавление нового столбца

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

  • содержащие операторы, которые вызовут потерю данных, например, удаление таблицы или столбца

Вы сами решаете, как разделить такие утверждения: поместить их в отдельные скрипты Liquibase или просто выбрать им нужную метку или контекст в редакторе.

Заключение

В этой статье мы посмотрели на первый релиз JPA Buddy. Очевидно, что как и любой новый продукт, JPA Buddy не может полностью избежать проблем и багов, особенно в своем первом релизе. Сейчас мы концентрируемся на устранении замечаний от пользователей, улучшении существующей функциональности, определении наиболее приоритетных функций, которые мы могли упустить из виду, и их реализации.

Говоря прямо, мы хотели бы вдохновить вас стать первыми пользователями JPA Buddy и сформировать сообщество энтузиастов. Установите JPA Buddy, попользуйтесь им и поделитесь своими отзывами с нашей командой разработчиков: это очень поможетнам выбрать правильное направление развития продукта. Также подпишитесь на наш Twitter: там мы показываем, какие еще фичи есть у JPA Buddy и как ими пользоваться. Думаю, вы найдете что-то полезное именно для вас.

Подробнее..

Hibernate и Spring Boot кто отвечает за имена таблиц?

28.04.2021 16:12:49 | Автор: admin

Когда мы добавляем зависимость в проект, мы подписываем контракт. Зачастую, многие условия в нем написаны мелким шрифтом. В этой статье мы рассмотрим кое-что, что легко пропустить при подписании трехстороннего контракта между вами, Hibernate и Spring Boot. Речь пойдет о стратегиях именования.

Значения по умолчанию в JPA

Главное правило для значений по умолчанию: они должны быть интуитивно понятными. Давайте проверим, следует ли этому правилу обычное приложение на Spring Boot с конфигурацией по умолчанию, Hibernate в качестве реализации JPA и PostgreSQL в качестве БД. Допустим, у нас есть сущность PetType. Давайте угадаем, как будет называться ее таблица в базе данных.

Первый пример:

@Entitypublic class PetType {    // fields omitted}

Я бы предположил, что именем таблицы станет имя класса, то есть PetType. Однако, после запуска приложения оказалось, что имя таблицы на самом деле pet_type.

Явно зададим имя с помощью @Table:

@Entity@Table(name = "PetType")public class PetType {    // fields omitted}

В этот раз имя точно должно быть PetType. Запустим приложение Таблица снова называется pet_type!

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

@Entity@Table(name = "\"PetType\"")public class PetType {  // fields omitted}

И опять наши ожидания не оправдались, имя снова "pet_type", но теперь в кавычках!

Стратегии именования Hibernate

На запрос имя таблицы поумолчанию для JPA-сущностей Google выдает следующий результат (англ.):

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

Именно это мы ожидали увидеть в первом примере, не так ли? Очевидно, что-то нарушает стандарт.

Углубимся в Hibernate. Согласно документации (англ.), в Hibernate есть два интерфейса, отвечающих за именование таблиц, столбцов и пр.: ImplicitNamingStrategy и PhysicalNamingStrategy.

ImplicitNamingStrategy отвечает за генерацию имен для всех объектов, которые не были явно названы разработчиком: имя сущности, имя таблицы, имя столбца, индекса, внешнего ключа и т.д.. Получившееся имя называется логическим, оно используется внутри Hibernate для идентификации объекта. Это не то имя, которое будет использовано в базе данных.

PhysicalNamingStrategy создает подлинное физическое имя на основе логического имени объекта JPA. Именно физическое имя используется в базе данных. Фактически, это означает, что с помощью Hibernate нельзя напрямую указать физическое имя объекта в БД, можно указать только логическое. Чтобы лучше понять, как это все работает, посмотрите на схему ниже.

По умолчанию, в Hibernate используются следующие реализации этих интерфейсов: ImplicitNamingStrategyJpaCompliantImpl и PhysicalNamingStrategyStandardImpl. Первый генерирует логические имена в соответствии со спецификацией JPA, а второй использует их как физические имена без каких-либо изменений. Лучше всего это описано в документации:

JPA определяет четкие правила автоматического именования. Если вам важна независимость от конкретной реализации JPA, или если вы хотите придерживаться определенных в JPA правил именования, используйте ImplicitNamingStrategyJpaCompliantImpl (стратегия по умолчанию). Кроме того, в JPA нет разделения между логическим и физическим именем. Согласно спецификации JPA, логическое имя это и есть физическое имя. Если вам важна независимость от реализации JPA, не переопределяйте PhysicalNamingStrategy.

Однако наше приложение ведет себя по-другому. И вот почему. Spring Boot переопределяет реализации Hibernate по умолчанию для обоих интерфейсов и вместо них использует SpringImplicitNamingStrategy и SpringPhysicalNamingStrategy.

SpringImplicitNamingStrategy фактически копирует поведение ImplicitNamingStrategyJpaCompliantImpl, есть только незначительное различие в именовании join таблиц. Значит, дело в SpringPhysicalNamingStrategy. В документации (англ.) указано следующее:

По умолчанию, в Spring Boot используется SpringPhysicalNamingStrategy в качестве физической стратегии именования. Эта реализация использует те же правила именования, что и Hibernate 4:
1. Все точки заменяются символами подчеркивания.
2. Заглавные буквы CamelCase приводятся к нижнему регистру, и между ними добавляются символы подчеркивания. Кроме того, все имена таблиц генерируются в нижнем регистре. Например, сущности TelephoneNumber соответствует таблица с именем telephone_number.

По сути, Spring Boot всегда преобразовывает camelCase и PascalCase в snake_case. Более того, использование чего-либо помимо snake_case вообще невозможно. Я бы не стал использовать camelCase или PascalCase для именования объектов базы данных, но иногда у нас нет выбора. Если ваше приложение на Spring Boot работает со сторонней базой данных, где используется PascalCase или camelCase, конфигурация Spring Boot по умолчанию для вас не подойдет. Обязательно убедитесь, что используемая PhysicalNamingStrategy совместима с именами в базе данных.

Получается, Hibernate соответствует спецификации JPA, а Spring Boot нет. Можно подумать, что это ошибка. Однако Spring Boot фреймворк, в котором множество решений уже принято за разработчика, в этом и есть его ценность. Другими словами, он имеет полное право по-своему реализовывать стандарты и спецификации тех технологий, которые он использует. Для разработчика это означает следующее:

  • Конечное поведение всегда определяется конкретной реализацией и может отличаться от спецификации.

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

  • Поведение по умолчанию может измениться с обновлением версии библиотеки, что может привести к непредсказуемым побочным эффектам;

Заключение

Магия конфигураций изкоробки это отлично, но может привести к неожиданному поведению. Один из способов избежать подобных проблем явно задавать все значения и не полагаться на автогенерацию. Для именования объектов JPA рекомендации следующие:

  1. Всегда называйте свои объекты JPA явно, чтобы никакая автоматическая стратегия именования не повлияла на ваш код.

  2. Используйте snake_case для имен столбцов, таблиц, индексов и других объектов JPA, чтобы все реализации PhysicalNamingStrategy никак их не преобразовывали.

  3. Если нельзя использовать snake_case (например, при использовании сторонней базы данных), используйте PhysicalNamingStrategyStandardImpl в качестве PhysicalNamingStrategy.

Еще один плюс явного именования объектов JPA вы никогда случайно не переименуете таблицу или атрибут в самой базе при рефакторинге Java-модели. Для этого придется менять имя в @Table или @Column.

Однако, введя эти правила мы просто переложили ответственность на разработчиков. Теперь нам надо следить, чтобы вся команда им следовала. К счастью, этот процесс можно автоматизировать с помощью инструментов разработки.

Если вы пользуетесь IntelliJ IDEA, попробуйте JPA Buddy плагин, который упрощает работу с JPA, Hibernate, Spring Data JPA, Liquibase и подобными технологиями. В настройках JPA Buddy есть специальная секция, в которой можно установить шаблоны для имен сущностей, атрибутов и пр.. Эти шаблоны применяются каждый раз, когда разработчики создают сущность или атрибут:

По задумке, настройки JPA Buddy хранятся в Git, таким образом все разработчики используют одни и те же шаблоны и следуют одним и тем же правилам.

Подробнее..

Как Spring Data работает с Redis

02.09.2020 18:14:51 | Автор: admin

Redis (Remote Dictionary Server) - заслужено считается старичком в мире NoSql решений. Этот пост про то, как Spring Data с ним работает. Идея написания данного поста возникла потому, что Redis не совсем похож на привычную базу, он поддерживает типы данных, которые не удобно использовать для хранения объектов(Кеш не в счет) и выполнять поиск по определенным полям. Здесь на примерах я постараюсь описать как с ним работает Spring Data посредством привычного CrudRepository и QueryDSL. Это не пример HowTo, которых множество. Кому интересны внутренности идем дальше.

Примеры будут основаны на простом проекте. Redis поднимается в докер контейнере, spring-boot приложение, которое тоже в контейнере с ним общается. Приложение содержит простую модель, репозиторий, сервис и контроллер. Потрогать все это можно через swagger на localhost:8080.
Кроме команд, которые сервис выполняет к базе я буду приводить еще небольшой псевдо-код, который более понятно описывает происходящее.

Работать мы будем с сущностью Student:

@Data@AllArgsConstructor@NoArgsConstructor@RedisHash("Student")public class Student {    @Id    private String id;    private String name;    private int age;}

Здесь, необходимо уточнить что аннотация @RedisHash("Student") говорит о том, под каким ключом будут агрегироваться все сущности.

Попробуем сохранить первого студента:

curl -X POST "http://localhost:8080/save" -H  "accept: */*" -H  "Content-Type: application/json" -d "{\"id\":\"1\",\"name\":\"Stephen\",\"age\":12}"

Выполнились 3 команды:

"DEL" "Student:1""HMSET" "Student:1" "_class" "com.odis.redisserviceweb.model.Student" "id" "1" "name" "Stephen" "age" "12""SADD" "Student" "1"

Мы видим, что первая команда - это "DEL" "Student:1", говорит о том что удали запись с ключом "Student:1". Этот ключ был сформирован с помощью значения аннотации @RedisHash + значение поля помеченного аннотацией @Id.

Далее следует "HMSET" "Student:1" "_class" "com.odis.redisserviceweb.model.Student" "id" "1" "name" "Stephen" "age" "12". Эта команда добавляет значения в хеш с именем "Student:1". На псевдо-коде это будет выглядеть как

Map "Student:1";"Student:1".put("_class", "com.odis.redisserviceweb.model.Student");"Student:1".put("id", "1");"Student:1".put("name", "Stephen");"Student:1".put("age", "12");

Ну и завершающая команда - это "SADD" "Student" "1" - добавляет в сет с именем "Student" значение "1".
В итоге что мы получили? Было создано два объекта в Redis. Первый - хеш с именем "Student:1", второй - сет с именем "Student".

Выполнив команду keys * - дай все ключи (Не выполнять на проде под страхом экзекуции) получим:

127.0.0.1:6379> keys *1) "Student"2) "Student:1"

Типы у этих объектов, как и было ранее описано:

127.0.0.1:6379> type "Student"set127.0.0.1:6379> type "Student:1"hash

Собственно - зачем два объекта? Сейчас все станет на свои места.

В поиске по @Id нет ничего необычного:

curl -X GET "http://localhost:8080/get/1" -H  "accept: */*"

Сформировался ключ "Student:1" и выполнилась команда, которая и вернула искомый объект:

"HGETALL" "Student:1"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "1"5) "name"6) "Stephen"7) "age"8) "12"

А теперь попробуем выполнить поиск всего, что мы сохранили, добавив перед этим еще одного студента:

curl -X POST "http://localhost:8080/save" -H  "accept: */*" -H  "Content-Type: application/json" -d "{\"id\":\"2\",\"name\":\"Macaulay\",\"age\":40}"curl -X GET "http://localhost:8080/get" -H  "accept: */*"

Выполнилось 3 команды:

"SMEMBERS" "Student"1) "1"2) "2""HGETALL" "Student:1"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "1"5) "name"6) "Stephen"7) "age"8) "12"127.0.0.1"HGETALL" "Student:2"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "2"5) "name"6) "Macaulay"7) "age"8) "40"

Сначала мы получили список всех ключей и после этого сделали запрос по каждому на получение значения. Именно поэтому было создано несколько объектов - "Student" - хранит все ключи, и по одному объекту на каждого студента с ключом "Student:@Id". Получается, что получение всех студентов имеет сложность O (N) где N - количество объектов в базе.

Удалим студунта:

curl -X DELETE "http://localhost:8080/delete/1" -H  "accept: */*"

Получаем:

"HGETALL" "Student:1"1) "_class"2) "com.odis.redisserviceweb.model.Student"3) "id"4) "1"5) "name"6) "Stephen"7) "age"8) "12""DEL" "Student:1"(integer) 1"SREM" "Student" "1"(integer) 1"SMEMBERS" "Student:1:idx"(empty array)"DEL" "Student:1:idx"(integer) 0

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

List<Student> findAllByName(String name);

Приложение поднялось сохраняем студента и делаем поиск по имени:

curl -X POST "http://localhost:8080/save" -H  "accept: */*" -H  "Content-Type: application/json" -d "{\"id\":\"1\",\"name\":\"Stephen\",\"age\":12}"curl -X GET "http://localhost:8080/get/filter/Stephen" -H  "accept: */*"

В ответе у нас пустой массив, а в логах запросов к Redis видим:

"SINTER" "Student:name:Stephen"(empty array)

Команда "SINTER" - Возвращает элементы сета, полученные в результате пересечения всех данных сетов, в нашем случае передан только один сет - "Student:name:Stephen" но мы о нем ничего не знаем и он не создавался.
Дело в том, что если мы хотим искать по полю, которое не помечено аннотацией @Id, это поле должно быть помечено аннотацией @Indexed и тогда Spring Data сделает дополнительные манипуляции при сохранении студента, т. к. понятие индекс в Redis отсутствует. Пометим поле name этой аннотацией:

@Data@AllArgsConstructor@NoArgsConstructor@RedisHash("Student")public class Student {    @Id    private String id;    @Indexed    private String name;    private int age;}

Теперь сохраним студента в чистую базу:

curl -X POST "http://localhost:8080/save" -H  "accept: */*" -H  "Content-Type: application/json" -d "{\"id\":\"1\",\"name\":\"Stephen\",\"age\":12}"

И в логах команд видим:

"DEL" "Student:1""HMSET" "Student:1" "_class" "com.odis.redisserviceweb.model.Student" "id" "1" "name" "Stephen" "age" "12""SADD" "Student" "1""SADD" "Student:name:Stephen" "1""SADD" "Student:1:idx" "Student:name:Stephen"

Первые три команды нам знакомы, но добавились еще две: создали сет "Student:name:Stephen" имя которого состоит из ключа, названия поля, помеченного аннотацией @Indexed и значением этого поля. В этот сет был добавлен Id этого студента. Если у нас появится студент с другим Id и именем Stephen его Id так же будет добавлен в этот сет. И был создан сет, который хранит все ключи индексов которые были созданы для этого объекта. Получилось что-то по типу:

Map "Student:1";"Student:1".put("_class", "com.odis.redisserviceweb.model.Student");"Student:1".put("id", "1");"Student:1".put("name", "Stephen");"Student:1".put("age", "12");Set "Student";"Student".add("1");Set "Student:name:Stephen";"Student:name:Stephen".add("1");Set "Student:1:idx";"Student:1:idx".add("Student:name:Stephen");

Теперь должно работать, Выполним поиск по имени и получим наше значение, в логах команд Redis будет:

"SINTER" "Student:name:Stephen""HGETALL" "Student:1"

Получаем Id студентов, и вытаскиваем их из хеша. Опять же количество операций прямо пропорционально количеству данных.

Так же мы можем искать по нескольким полям для этого в качестве аргумента команды SINTER будут передаваться два объекта. Команда вернет пересекающееся id и выполнит по ним поиск.
В примере есть метод поиска по имени и возрасту.

Как видим, Spring Data достаточно неплохо интерпретирует работу с объектами и индексами в Redis. Данный подход можно перенять и не используя решение Spring.
Недостаток - это то, что поля по которым будет проводится поиск должны быть промаркированы аннотацией @Indexed с самого начала. В противном случае, "индексы" будут созданы только для объектов, которые сохраняются после добавления этой аннотации. И да, я понимаю, что Redis это не лучшее решения для таких нужд, но если в силу определенной ситуации его необходимо будет использовать, то SpringData сумеет это сделать достаточно неплохо.

Подробнее..
Категории: Nosql , Redis , Java , Spring , Spring data

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru