Рубрики
Без рубрики

Как сопоставить тип столбца диапазона PostgreSQL с JPA и гибернацией

Узнайте, как сопоставить тип столбца диапазона PostgreSQL при использовании JPA и Hibernate. Проект OSS типа hibernate очень удобен для этой цели.

Автор оригинала: Vlad Mihalcea.

В этой статье мы рассмотрим, как сопоставить типы столбцов диапазона PostgreSQL с помощью JPA и Hibernate.

К счастью, вам не нужно реализовывать пользовательский тип гибернации для типа столбцов PostgreSQL range , поскольку проект hibernate-types уже обеспечивает его поддержку.

Как наследовать свойства от сущности базового класса с помощью @MappedSuperclass с JPA и гибернацией @vlad_mihalcea https://t.co/1mM0bcwNFa pic.twitter.com/lZ9csG8hJS

PostgreSQL поддерживает несколько диапазонов типов :

  • int4range – Хранит диапазон целых значений
  • int8range – Хранит диапазон bigint (например, java.util.Длинные ) значения
  • numrange – Хранит диапазон числовых (например, java.util.BigDecimal ) значений
  • диапазон дат – Хранит диапазон меток времени (например, java.time.Значения LocalDate )
  • tsrange – Хранит диапазон меток времени (например, java.time.Значения LocalDateTime )
  • tstzrange – Хранит диапазон меток времени с часовым поясом (например, java.time.Значения ZonedDateTime )

Для представления диапазонов значений, которые могут иметь открытые или закрытые нижние и верхние границы, мы можем использовать класс Range , поставляемый с проектом hibernate-types .

При сопоставлении пользовательского типа Hibernate у вас есть два варианта:

  • вы можете реализовать интерфейс типа пользователя
  • вы можете расширить тип AbstractSingleColumnStandardBasicType

Используя прежнюю стратегию, тип диапазона PostgreSQL выглядит следующим образом:

public class PostgreSQLRangeType 
        extends ImmutableType {

    public PostgreSQLRangeType() {
        super(Range.class);
    }

    @Override
    public int[] sqlTypes() {
        return new int[]{Types.OTHER};
    }

    @Override
    protected Range get(
            ResultSet rs, 
            String[] names, 
            SharedSessionContractImplementor session, 
            Object owner) 
        throws SQLException {
        
        Object pgObject = rs.getObject(names[0]);
        
        String type = ReflectionUtils.invokeGetter(
            pgObject, 
            "type"
        );
        
        String value = ReflectionUtils.invokeGetter(
            pgObject, 
            "value"
        );

        switch (type) {
            case "int4range":
                return Range.integerRange(value);
            case "int8range":
                return Range.longRange(value);
            case "numrange":
                return Range.bigDecimalRange(value);
            case "tsrange":
                return Range.localDateTimeRange(value);
            case "tstzrange":
                return Range.zonedDateTimeRange(value);
            case "daterange":
                return Range.localDateRange(value);
            default:
                throw new IllegalStateException(
                    "The range type [" + type + "] is not supported!"
                );
        }
    }

    @Override
    protected void set(
            PreparedStatement st, 
            Range range, 
            int index, 
            SharedSessionContractImplementor session) 
        throws SQLException {

        if (range == null) {
            st.setNull(index, Types.OTHER);
        } else {
            Object holder = ReflectionUtils.newInstance(
                "org.postgresql.util.PGobject"
            );

            ReflectionUtils.invokeSetter(
                holder, 
                "type", 
                determineRangeType(range)
            );
            
            ReflectionUtils.invokeSetter(
                holder, 
                "value", 
                range.asString()
            );

            st.setObject(index, holder);
        }
    }

    private static String determineRangeType(Range range) {
        Class clazz = range.getClazz();

        if (clazz.equals(Integer.class)) {
            return "int4range";
        } else if (clazz.equals(Long.class)) {
            return "int8range";
        } else if (clazz.equals(BigDecimal.class)) {
            return "numrange";
        } else if (clazz.equals(LocalDateTime.class)) {
            return "tsrange";
        } else if (clazz.equals(ZonedDateTime.class)) {
            return "tstzrange";
        } else if (clazz.equals(LocalDate.class)) {
            return "daterange";
        }

        throw new IllegalStateException(
            "The class [" + clazz.getName() + "] is not supported!"
        );
    }
}

Если вы хотите реализовать пользовательский тип гибернации, реализовав интерфейс UserType , будет намного проще, если вы просто расширите Неизменяемый тип , предлагаемый проектом hibernate-types .

Для получения более подробной информации ознакомьтесь с этой статьей .

Как уже упоминалось, вам не нужно создавать классы диапазона или типа диапазона//PostgreSQL. Вы можете получить их с помощью типов гибернации зависимости Maven:


    com.vladmihalcea
    hibernate-types-55
    ${hibernate-types.version}

Если вы используете более старую версию Hibernate, перейдите в репозиторий hibernate-types GitHub и найдите соответствующую зависимость hibernate-types для вашей текущей версии Hibernate.

Давайте предположим, что мы разрабатываем приложение для книжного магазина, и объекты Книга выглядят следующим образом:

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(
    typeClass = PostgreSQLRangeType.class, 
    defaultForType = Range.class
)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    private String title;

    @Column(
        name = "price_cent_range", 
        columnDefinition = "numrange"
    )
    private Range priceRange;

    @Column(
        name = "discount_date_range", 
        columnDefinition = "daterange"
    )
    private Range discountDateRange;

    //Getters and setters omitted for brevity
}

Обратите внимание на использование аннотации @TypeDef , которая указывает Hibernate использовать тип диапазона PostgreSQL Тип гибернации для обработки свойств диапазона сущности.

Свойство isbn помечено аннотацией @NaturalID Hibernate, которая позволяет нам извлекать сущность Book с помощью ее естественного идентификатора. Для получения более подробной информации об использовании натуральных идентификаторов ознакомьтесь с этой статьей .

используется для того, что очень полезно для сопоставления бизнес – ключей.

Теперь, когда сохраняются следующие две Книги сущности:

Book book = new Book();
book.setIsbn("978-9730228236");
book.setTitle("High-Performance Java Persistence");
book.setPriceRange(
    Range.closed(
        BigDecimal.valueOf(39.95d),
        BigDecimal.valueOf(45.95d)
    )
);
book.setDiscountDateRange(
    Range.closedOpen(
        LocalDate.of(2019, 11, 29),
        LocalDate.of(2019, 12, 3)
    )
);

entityManager.persist(book);

Hibernate создает следующие инструкции SQL INSERT:

INSERT INTO book (
    discount_date_range, 
    isbn, 
    price_cent_range, 
    title, 
    id
)
VALUES (
    '[2019-11-29,2019-12-03)',
    '978-9730228236',
    '[39.95,45.95]',
    'High-Performance Java Persistence',
    1
)

При извлечении ранее сохраненной сущности Book мы видим, что свойства range правильно извлечены из базовых столбцов базы данных:

Book book = entityManager
.unwrap(Session.class)
.bySimpleNaturalId(Book.class)
.load("978-9730228236");

assertEquals(
    BigDecimal.valueOf(39.95d), 
    book.getPriceRange().lower()
);

assertEquals(
    BigDecimal.valueOf(45.95d), 
    book.getPriceRange().upper()
);

assertEquals(
    LocalDate.of(2019, 11, 29), 
    book.getDiscountDateRange().lower()
);

assertEquals(
    LocalDate.of(2019, 12, 3), 
    book.getDiscountDateRange().upper()
);

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

List discountedBooks = entityManager
.createNativeQuery(
    "SELECT * " +
    "FROM book b " +
    "WHERE " +
    "   b.discount_date_range @> CAST(:today AS date) = true ", Book.class)
.setParameter(
    "today", 
    LocalDate.of(2019, 12, 1)
)
.getResultList();

assertTrue(
    discountedBooks.stream().anyMatch(
        book -> book.getTitle().equals("High-Performance Java Persistence")
    )
);

Круто, правда?

Сопоставление нестандартных типов столбцов базы данных с помощью Hibernate довольно просто. Однако с помощью проекта hibernate-types вам даже не нужно писать все эти типы.

Просто добавьте зависимость Maven в свой проект pom.xml файл конфигурации и укажите аннотацию @TypeDef для рассматриваемой сущности и начните сопоставление JSON , МАССИВА , PostgreSQL Перечисления или Inet адресов.