Что, если у нас в базе данных PostgreSQL есть столбец, содержащий данные типа JSON, но нет ни соответствующего ему типа данных в Java, ни механизма преобразования в Hibernate из какого-то встроенного/пользовательского типа/объекта Java в JSON и обратно?
Тогда у нас есть 2 варианта*:
1) создать самим механизм преобразования JSON-String Java в JSON PostgreSQL;
2) использовать разработанную Vlad Mihalcea библиотеку hibernate-types-52.
Первый вариант:
1\ Создаём свой диалект PostgreSQL:
2\ Указываем наш диалект в файле application.properties:
3\ Создаём свою реализацию интерфейса org.hibernate.usertype.UserType. Ниже приведена упрощённая реализация данного интерфейса, т.к. класс будет "заточен" для мапинга объекта только одного типа (java.lang.String):
4\ Аннотируем объект и соответствующее поле аннотациями @TypeDefs и @Type:
5\ Создаём обычную реализацию CrudRepository.
Второй вариант:
1\ Подключаем зависимость от библиотеки hibernate-types-52:
2\ Мне потребовалось подключить также зависимости от библиотек jackson-core, jackson-databind и jackson-annotations:
3\ Объявляем новые типы с помощью аннотации @TypeDefs над классом-Entity.
С помощью аннотации @Type над нужным полем класса прописываем мапинг поля в JSON:
4\ Создаём обычную реализацию CrudRepository:
5\ Тестируем:
6\ В базе данных находим вот такую запись:
Весь код можно посмотреть в репозиториях на GitHub:
- вариант 1;
- вариант 2.
* На самом деле, вариантов мапинга больше, чем два. Я выбрал те, которые показались наиболее простыми.
Тогда у нас есть 2 варианта*:
1) создать самим механизм преобразования JSON-String Java в JSON PostgreSQL;
2) использовать разработанную Vlad Mihalcea библиотеку hibernate-types-52.
Первый вариант:
1\ Создаём свой диалект PostgreSQL:
public class JsonPostgreSQLDialect extends PostgreSQL9Dialect { public JsonPostgreSQLDialect() { super(); this.registerColumnType(Types.JAVA_OBJECT, "json"); } }
2\ Указываем наш диалект в файле application.properties:
spring.jpa.properties.hibernate.dialect=ru.epatko.postgresjson.stringjsontype.JsonPostgreSQLDialect
3\ Создаём свою реализацию интерфейса org.hibernate.usertype.UserType. Ниже приведена упрощённая реализация данного интерфейса, т.к. класс будет "заточен" для мапинга объекта только одного типа (java.lang.String):
public class StringJsonUserType implements UserType { @Override public int[] sqlTypes() { return new int[] {Types.JAVA_OBJECT}; } @Override public Class returnedClass() { return String.class; } @Override public boolean equals(Object x, Object y) throws HibernateException { if(x == null) return y == null; return x.equals(y); } @Override public int hashCode(Object x) throws HibernateException { return x.hashCode(); } @Override public Object nullSafeGet(ResultSet resultSet, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { return resultSet.getString(names[0]); } @Override public void nullSafeSet(PreparedStatement statement, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { if (value == null) { statement.setNull(index, Types.OTHER); } else { statement.setObject(index, value, Types.OTHER); } } @Override public Object deepCopy(Object value) throws HibernateException { return value; } @Override public boolean isMutable() { return true; } @Override public Serializable disassemble(Object value) throws HibernateException { return (String) value; } @Override public Object assemble(Serializable cached, Object o) throws HibernateException { return cached; } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; } }
4\ Аннотируем объект и соответствующее поле аннотациями @TypeDefs и @Type:
@AllArgsConstructor @NoArgsConstructor @Data @Entity @TypeDefs({@TypeDef(name = "StringJsonObject", typeClass = StringJsonUserType.class)}) public class MyModel { @Id private long id; @Type(type = "StringJsonObject") private String details; //String of JSON object //Example: "{\"value1\":1,\"value2\":\"abc\"}" }
5\ Создаём обычную реализацию CrudRepository.
@Repository public interface MyRepository extends CrudRepository<MyModel, Long> { }
Второй вариант:
1\ Подключаем зависимость от библиотеки hibernate-types-52:
<dependency> <groupId>com.vladmihalcea</groupId> <artifactId>hibernate-types-52</artifactId> <version>2.3.5</version> </dependency>
2\ Мне потребовалось подключить также зависимости от библиотек jackson-core, jackson-databind и jackson-annotations:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${jackson.version}</version> </dependency>
3\ Объявляем новые типы с помощью аннотации @TypeDefs над классом-Entity.
С помощью аннотации @Type над нужным полем класса прописываем мапинг поля в JSON:
@Data @Entity @TypeDefs({ @TypeDef(name = "json", typeClass = JsonStringType.class), @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) }) @NoArgsConstructor public class MyNewModel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Type(type = "jsonb") @Column(columnDefinition = "json") private Details details; public MyNewModel(Details details) { this.details = details; } }
4\ Создаём обычную реализацию CrudRepository:
@Repository public interface MyRepository extends CrudRepository<MyNewModel, Long> { }
5\ Тестируем:
@RunWith(SpringRunner.class) @SpringBootTest public class MyRepositoryTest { @Autowired private MyRepository repository; @Test public void saveModel() { String name = "name"; String email = "email"; int age = 1; Details details = new Details(email, name, age); MyNewModel model = new MyNewModel(details); MyNewModel saved = repository.save(model); assertEquals(saved, model); } }
6\ В базе данных находим вот такую запись:
- вариант 1;
- вариант 2.
* На самом деле, вариантов мапинга больше, чем два. Я выбрал те, которые показались наиболее простыми.