вторник, 4 декабря 2018 г.

Hibernate & JSON type in PostgreSQL

Что, если у нас в базе данных PostgreSQL есть столбец, содержащий данные типа JSON, но нет ни соответствующего ему типа данных в Java, ни механизма преобразования в Hibernate из какого-то встроенного/пользовательского типа/объекта Java в JSON и обратно?

Тогда у нас есть 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\ В базе данных находим вот такую запись:


Весь код можно посмотреть в репозиториях на GitHub:
- вариант 1;
- вариант 2.

* На самом деле, вариантов мапинга больше, чем два. Я выбрал те, которые показались наиболее простыми.

Грокаем алгоритмы

В "Грокаем алгоритмы. Иллюстрированное пособие для программистов и любопытствующих" (Адитья Бхаргава) простым доступным языком объясняется работа нескольких наиболее популярных алгоритмов, в том числе и тех, что применяются в машинном обучении.

Книга интересная, читается на одном дыхании и оставляет после себя чувство небольшого интеллектуального голода - хочется изучать алгоритмы ещё и ещё, комбинировать и создавать их самому и применять везде, где только можно "без ущерба для производства". ))

Книга, купив которую, не пожалел ни разу

Бывает такое: покупаешь книгу - вроде бы интересная и даже про то, что должно пригодиться в работе, но читаешь её и не понимаешь, где именно в своём проекте ты можешь применить эти знания. Либо случаи в книге описываются очень специфические, либо слишком уж упрощённые примеры кода приводятся. В общем, не "заходит" книга.

Так вот,  "Java Persistence API и Hibernate" (Бауэр Кристиан, Гэвин Кинг, Гэри Грегори) не из таких. На каждой её странице можно бить себя ладонью по лбу и восклицать: "Вот оно! Вот, как надо! Ну, теперь уж точно всё будет красиво!" Почти на каждую страницу можно приклеивать закладку с тремя восклицательными знаками, чтобы не забыть, куда посмотреть перед тем, как начать кодить.

Эта книга - букварь Hibernate, в котором от А до Я рассказывается всё: от азов отображения объектов на схему реляционной базы данных, до производительности запросов. Пока что не видел ни одного видео-курса, который бы так полно охватывал и подробно раскрывал столько тем по JPA и Hibernate.

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

В общем, читать - обязательно. Правда, есть вероятность, что вас перестанут понимать коллеги по работе. )