Автор оригинала: 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()
);
Что хорошо в использовании типов столбцов диапазон , так это то, что мы можем использовать операторы, зависящие от диапазона, такие как @> , который проверяет, содержится ли указанное значение в интервале диапазона:
ListdiscountedBooks = 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 адресов.