Что такое Hibernate Framework в Java и как его использовать?

Сергій Бондаренко

Сегодня мы рассмотрим важный инструмент для взаимодействия с базами данных — Hibernate. Вы узнаете что это за библиотека, в каких случаях и как используется, а также как реализована работа с БД в Java.

Но перед тем, как перейти к разговору о самом Hibernate, нужно сказать пару слов о том, что означает ORM, JDBC и некоторых принципах работы с БД.

JDBC — API: управление запросами к СУБД

Когда Java-программа устанавливает связь с базой данных, она не посылает запросы к таблицам напрямую, а использует программный интерфейс JDBC (Java Database Connectivity). Подключение к БД и дальнейшее взаимодействие с ней происходит через специальные драйверы. Вместо того, чтобы создавать отдельный набор методов и процедур, которые будут работать с конкретной базой данных (как например, это сделано в PHP — где есть отдельные наборы процедур для MySQL, Postgres и прочих БД), в Java был придуман единый интерфейс.

Он позволяет любой Java-программе иметь дело с разными реляционными базами через абсолютно одинаковые методы. Чтобы в каждом случае работа шла с едиными методами, должны быть задействованы JDBC-драйверы. Динамически подгружаясь по ходу выполнения приложения, такой драйвер автоматически инициализируется и вызывается, когда приложение запрашивает URL, включающий протокол, за который ответственен драйвер.

Код обычного драйвера JDBC MySQL под платформу Linux имеет вид:

package javaapplication1;
import java.sql.*;
public class Main {

    public static void main(String[] args) throws SQLException {
        /**
         * Этот код выполняет загрузку драйвера DB.
               */        //Class.forName("com.mysql.jdbc.Driver");
        Connection conn = null;

        try {
            conn = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/db_name",
            "user", "password");
            if (conn == null) {
                System.out.println("Отсутствует коннект с БД!");
                System.exit(0);
            }

                        Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM users");
                while (rs.next()) {
                System.out.println(rs.getRow() + ". " + rs.getString("firstname")
                        + "\t" + rs.getString("lastname"));
            }
                stmt.close();
        }
        catch (SQLException e) {
            e.printStackTrace();
        } finally{
            if (conn != null){
                conn.close();
            }
        }
    }
}

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

Обязанность по созданию JDBC-драйвера лежит на вендоре базы данных. К любой БД такой драйвер уже существует и поддерживается разработчиком, его легко можно найти в интернете.

Набор операций JDBC стандартный и очень простой:

  • подключились к базе данных;
  • создали statement;
  • выполнили SQL-запрос и получили Result set;
  • прошлись циклом и получили все данные, которые извлекли из БД, если делали запрос select.

Существует ряд фундаментальных проблем, связанных с работой языка Java с реляционными БД. Язык Java использует парадигму ООП, поэтому мы все время имеем дело с объектами. В то же время реляционные базы данных оперируют таблицами.

Объекты в Java и таблицы в БД — это разные сущности, которые сложно между собой сопоставить. С другой стороны, для решения задач нам постоянно нужно каким-то образом объекты превращать в таблицы и наоборот. Задача нетривиальная, решить которую сложно — нужно писать некоторую логику, используя тот же самый JDBC. 

Преимущества JDBC:

  • единый интерфейс для работы СУБД, одни и те же классы для всех БД;
  • простой синтаксис;
  • кроссплатформенное решение (MacOS, Linux, Windows);
  • отсутствие необходимости в стороннем ПО, достаточно одного JDBC-драйвера.

Недостатки JDBC:

  • не годится для больших проектов;
  • сложно использовать концепцию MVC;
  • возможны трудности при переходе от одной СУБД к другой (с имеющимся кодом);
  • отличия в синтаксисе SQL-запросов.

В простых случаях применять JDBC не сложно (например, был объект «Пользователь», и для того, чтобы записать его в базу данных, мы из некоторой строки достаем id, имя и т.д.). Но если мы будем иметь дело со сложным графом объектов, сохранить его в базу данных будет уже не так легко — придется писать длинный код SQL, который потом проверить будет очень сложно. Кроме того, мы имеем дело в Java классом based object oriented language, где есть наследование, а в таблицах нет никакого наследования.

Если вы пишете серьезное приложение, то вы будете работать с кодом на огромное число строк. Вся современная бэкенд-разработка — это работа с большим количеством информации. Как следствие — разработчик имеет дело с постоянными запросами к БД. Ему необходимо чуть ли не в каждой строчке написать connect, получить, извлечь данные из базы, затем строки и цифры нужно разложить по полям объектов. Серьезный enterprise-объект может содержать до десяти тысяч полей и, соответственно, у вас будет десять тысяч строк. Ситуация осложняется тем, что такие объекты постоянно меняются — по этой причине приходится решать массу проблем. 

Что такое Hibernate в Java и для чего он нужен

Объектная модель данных и модель данных в реляционных таблицах не очень совпадают. Имеется некая логика сравнения объектов, и никто не гарантирует, что эта логика совпадает и в базе данных, и в приложении. Это может приводить к большому числу ошибок.

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

Мы обязаны следить, чтобы в объектной модели Java и той, которая хранится в базе, сущности совпадали, чтобы была одна и та же логика сравнения.

В объектной модели данных Java также имеются некие дополнительные возможности — например, разные уровни доступа к разным полям. Некоторые поля мы бы не хотели иметь в БД, некоторые мы бы не хотели извлекать или записывать.

Чтобы не кодить лишний раз и думать про JDBC, была взята концепция ORM.

Object-Relational Mapping — это механизм, который позволяет отображать данные из реляционных баз данных в виде объектов. ORM-системы упрощают работу с базами данных, уменьшая необходимость руководствоваться SQL-командами.

Все фреймворки в Java стандартизированы и работают на строгих спецификациях. Это нужно, чтобы не было хаоса и разных трактовок той или иной технологии. Такой принцип во многом похож на ООП, когда мы сначала создаем интерфейс, что по сути — спецификация или правило работы объектов, и только потом мы создаем любое количество реализаций.

В Java присутствует спецификация JPA (Java Persistence API). Она определяет способы для управления данными и содержит инструменты:

  • CRUD API — программный интерфейс для обращения к популярным методам объекта который хранится в БД (insert, select и пр.), стандартных операций (создание, чтение, обновление и удаление).
  • Объектно-ориентированный язык запросов. Некий доменный язык, похожий на SQL — удобный, чтобы говорить про объекты (а не про таблицы).

Чтобы реализовать JPA нам понадобится так называемые JPA-провайдеры — библиотеки, которые его реализуют.

Таких провайдеров существует много для разных языков программирования:

  • Enterprise JavaBeans Entity Beans
  • Java Data Objects
  • Castor
  • TopLink
  • Spring DAO
  • MyBatis
  • Hibernate

Более половины имплементаций JPA выполнено на фреймворке Hibernate с открытым кодом, чем объясняется популярность этой библиотеки.

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

Другими словами: производится объектно-реляционный мэппинг (ORM). Благодаря Hibernate (который использует программный интерфейс JDBC) программисты не задумываются, как поля из базы данных попадают в поля объектов.

Особенности Hibernate

Для работы Hibernate использует HQL — эквивалент языка SQL, только с учетом Java-классов. Фреймворк может применяться в любом типе приложений — desktop/web, Spring и так далее. Сегодня Hibernate портирован на другие языки, например, в C# (.NET) используется Nhibernate.

Еще одна из особенностей Hibernate — механизм «ленивой загрузки». Он позволяет отложить загрузку связанных сущностей, пока они не будут явно использованы. Отложенные SQL-запросы будут выполняться только тогда, когда связанные сущности (ключи) будут явно использованы.

Подведем итог. Hibernate применяется в Java-разработке, когда возникает необходимость перенести из базы данных информацию в код и как-то ее обработать. Этот фреймворк помогает оптимизировать низкоуровневый код, ускорив его написание и сделав его более компактным и удобным для разработчиков.

Что нужно для начала использования Hibernate

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

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

Представление Java-классов с таблицами БД реализуется посредством XML-файлов с конфигурациями либо через Java-аннотации.

Когда данных много, связи между сущностями добавляют головной боли. В Hibernate есть стандартный набор связей, которые могут понадобиться:

  • One-to-one
  • One-to-many (many-to-one)
  • Many-to-many

Перед тем, как приступить к работе с Hibernate, вам нужно изучить:

  • JDBC
  • SQL
  • ООП
  • XML
  • Аннотации в Java
  • Разобраться с настройками параметров проекта в IDE

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

1. Создать проект и подключить зависимости.
2. Добавить параметры для работы Hibernate.
3. Выполнить представление (мapping) классов для связывания таблиц БД с кодом.
4. Создать SQL-запросы для бизнес-процессов (select, update, delete и т.д.)
5. Обработать результаты запросов.
6. Представить результаты в нужном формате во внешнем графическом интерфейсе (web, Android, десктопное приложение и т.д.).

Кэширование в Hibernate

Один из способов оптимизации и ускорения работы приложения — кэширование.

В Hibernate используется три уровня кэшей:

  1. Кэш первого уровня (сессионный) — это то, что включено по умолчанию, работает всегда и то, что нельзя отключить. Он выполняет кэширование сущностей сохраняя в persistence context. Допустим, вы хотите выгрузить сущность из БД. Она выгружается один раз и попадает в persistence context. Вы тут же хотите выгрузить еще раз, и Hibernate уже не обратится к БД, а возьмет из persistence context. При работе с этим типом кэширования нередко возникают сайд-эффекты. Например, он хорошо работает через EntityManager. Но бывает так, что если вы меняете сущности вне EntityManager, кто-нибудь в другой транзакции обращается к БД и меняет сущность. В первой транзакции она выгружена и, если вы захотите проверить, является ли она подходящей под ваши параметры, сущность уже будет иной. Вам про это будет неизвестно, потому что у вас будет задействован кэш первого уровня.
  2. Кэш второго уровня — примерно то же самое. Он должен быть явно включен и настроен. Кэш второго уровня привязан к объекту-фабрике сессий (Session Factory Object). Среди вариантов реализации кэша второго уровня: EHCache, OSCache, SwarmCache, JBoss, TreeCache.
  3. Кэш третьего уровня (кэш запросов) — кэширует результаты запросов по запросу и его параметрам. Он сохраняет не сами объекты целиком, а их id. Поэтому, чтобы их получить, ему удобнее связаться через кэш второго уровня, чем делать запросы в БД.

Преимущества и недостатки Hibernate

Hibernate, безусловно, удобен. Самые популярные IDE (среды разработки) поддерживают этот фреймворк через плагины. Но хотя библиотека и помогает нам убрать «спагетти» из кода, упростив его, она все же имеет ряд недостатков:

  • Hibernate — довольно громоздкое решение, поэтому при некорректных параметрах в крупных проектах, где идет работа с большим количеством объектов, возможно снижение производительности.
  • Для JPA и Hibernate есть определенный набор API, который следует изучить.
  • При работе с библиотекой на низком уровне у вас меньше контроля доступа к БД.

В Hibernate часто встречаются конфигурации по умолчанию — имена таблиц, столбцов и т.д. Кроме того, для некоторых параметров фреймворк сам «догадывается» про требуемое значение.

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

Создаем проект и настраиваем Hibernate

Поскольку у Hibernate — не одна библиотека, а несколько, следует их все подключить. Наш проект будем использовать со сборщиком Maven, который будет отвечать за загрузку необходимых нам библиотек.

Maven создаст структуру проекта, и Hibernate будет ее использовать, включая конфигурационный файл проекта pom.xml, содержащий информацию о зависимостях и прочие опции. Внутри папки java будем писать java-классы, в папке resources будут находится все файлы конфигурации Hibernate.

Файл pom.XML выглядит так:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.app.HibernateTest</groupId>
    <artifactId>QuickStart</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>19</maven.compiler.source>
        <maven.compiler.target>19</maven.compiler.target>
    </properties>

</project>

В разделе зависимостей (dependency) мы должны указать, что мы будем работать с Hibernate:

Переходим на https://mvnrepository.com/ и вводим в поиске Hibernate Core. Выбираем версию, наш сборщик, затем копируем зависимости и вставляем в файл pom.xml:

<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.1.6.Final</version>
    <type>pom</type>
</dependency>

Далее по поиску в репозитории ищем соответствующие зависимости для коннектора БД и вставляем их тоже:

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>

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

Создадим класс Student, содержащий поля, которые нужно промапить. Добавим публичный конструктор и переопределим метод:

package com.app.pojo;

public class Student {
    private int id;
    private String name;
    private int age;

    public  Student(){}

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

Перед началом работы с базой пропишите параметры в файле hibernate.cfg в каталоге resources. Есть еще альтернативный вариант с xml-настройками — через файл persistence.xml. Он применяется для любой реализации JPA (в том числе и Hibernate), но он, понятное дело, ограничен спецификацией JPA.

Создадим hibernate.cfg:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-configuration PUBLIC 
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN" 
  "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<!-- Version 8 MySQL hiberante-cfg.xml example for Hibernate 5 -->
<hibernate-configuration>
  <session-factory>
    <property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property>
    <!-- property name="connection.driver_class">com.mysql.jdbc.Driver</property →

//Указываем, где искать драйвер;
    <property name="connection.url">jdbc:mysql://localhost:3306/mydb</property>

//Путь, по которому будет идти запрос при обращении к базе данных (для данного примера - testdatabase на порту 3306, имя БД - mydb)

    <property name="dialect">org.hibernate.dialect.MySQL8Dialect</property>
    <property name="connection.username">root</property>

//Пользователь

    <property name="connection.password">root</property>

//Пароль

    <property name="connection.pool_size">3</property>
    <!--property name="dialect">org.hibernate.dialect.MySQLDialect</property→

//Диалект для БД, к которой будет идти подключение;

    <property name="current_session_context_class">thread</property>
    <property name="show_sql">true</property>//отображение SQL-запросов;
    <property name="format_sql">true</property>
    <property name="hbm2ddl.auto">update</property>

//Поле, необходимое для того, чтобы дать разрешение Hibernate на обновление данных в БД. Если данных не будет, он их создаст.

    <!-- mapping class="com.mcnz.jpa.examples.Player" / -->

  </session-factory>
</hibernate-configuration>

Внутри конфигурационного файла создается секция session-factory — фреймворк открывает сессию, в течение которой делает запросы. Как только сессия будет закончена, он ее будет закрывать. Порядок свойств (property) в конфигурационном файле не имеет значения. 

Для получения сессии можно использовать два главных объекта — SessionFactory (в JPA — EntityManagerFactory) и Session (в JPA — EntityManager). SessionFactory создается только один раз при запуске приложения, он настривать, работать с сессиями. При создании SessionFactory считываются параметры hibernate.cfg.xml.

Создадим еще один класс — наше Java-приложение App. Id для студента создавать не будем, этот идентификатор будет автоматически создаваться в БД одновременно с появлением новой записи.

Обратите внимание: уникальный ключ должен быть всегда, если мы его создаем, в БД он создаваться не должен, иначе в базе данных будут ошибки.

Мы хотим передать данные нашего студента в таблицу. Соответственно, нам желательно было бы написать что-то простое, например save(Serge):

package com.app;

import com.app.pojo.Student
import org.hibernate.cfg.Configuration;
public class App {
    public static void main(String[] args) {
        Student Serge = new Student();
        Serge.setName("Serge");
        Serge.setAge(10);

        //Serge.setId();
        Configuration con = new Configuration().configure;
        con.addAnnotatedClass (Student.class);

        StandardServiceRegistryBuilder sBilder = new StandardServiceRegistryBuilder()
                .applySettings(con.getProperties());
        SessionFactory sf = con.buildSessionFactory(sBilder.build());
    }
}

Пример создания CRUD-приложения (Create, Read, Update, Delete)

Если это необходимо, в конфигурационном файле мы можем написать, сколько фабрика сессий может генерировать потоков для работы с клиентами. После того, как фабрика сессий прописана, можем переходить к CRUD.

Create

Начнем с команды Create:

public class App {

    public static void main(String[] args) {

        Student Serge = new Student();
        Serge.setName("Serge");
        Serge.setAge(10);

        //Serge.setId();
        Configuration con = new Configuration().configure;
        con.addAnnotatedClass (Student.class);

        StandardServiceRegistryBuilder sBilder = new StandardServiceRegistryBuilder()
                .applySettings(con.getProperties());
        SessionFactory sf = con.buildSessionFactory(sBilder.build());

        //create
        Session sessionCreate = sf.openSession();
        Transaction trCreate = sessionCreate.beginTransaction;//Начало транзакции
        sessionCreate.save(Serge);
        trCreate.commit();
        sessionCreate.close();//Завершение транзакции
    }
}

Напомним, транзакции позволяют сохранять состояние базы данных. Они удостоверяют, что изменения, сделанные в рамках транзакции, сохранятся, или, наоборот — изменения, сделанные в рамках транзакции, будут отклонены.

Поэтому между созданием и коммитом транзакции мы можем сохранять сразу несколько объектов. Посмотрим, как работает теперь наше приложение. Возьмем базу данных (принципиального значения, что это будет за БД не имеет), подключимся к ней и посмотрим на результат:

Для просмотра подойдет какая-нибудь внешняя программа, например, MySQL Workbench, а можно подключиться к ней прямо в среде разработки IDE (скажем, в IntelliJ IDEA):

В БД появились новые колонки и пользователь Serge. Значение id присвоено автоматически. При повторном добавлении пользователя автоматически присваивается новый id. Помимо основной таблицы Hibernate создал также еще одну — hibernate_sequence. Это промежуточная таблица, в которой хранится инкрементируемое значение value.

Read

Транзакция прошла, данные добавлены. Переходим к команде Read и открываем новую сессию:

//read

        Session sessionRead = sf.openSession();
        Transaction trRead = sessionRead.beginTransaction();
        Student student1 = sessionRead.find(Student.class, o:1);
        System.out.println(student1.toString());
        trRead.commit();
        sessionCreate.close();

Результат вывода в консоль:

Student{id=1, age=10, name=Serge}

Update

Следующая команда Update:

      //update

        Session sessionUpdate = sf.openSession();
        Transaction trUpdate = sessionUpdate.beginTransaction();
        Student student2 = sessionUpdate.find(Student.class, o:2);
        student2.setAge(20);
        student2.setName("Alexander");
        sessionUpdate.update(student2);
        trUpdate.commit();
        sessionUpdate.close();

Пользователь в базе данных обновит свое имя и значение Age.

Delete

Последняя транзакция — Delete:

//delete

        Session sessionDelete = sf.openSession();
        Transaction trDelete = sessionDelete.beginTransaction();
        sessionDelete.delete(student2);
        txDelete.commit();
        sessionDelete.close();

Запись в таблице будет уничтожена.

Заключение

Hibernate — это удобный, мощный и эффективный ORM-фреймворк. Он прост в изучении и существенно экономит время при разработке ПО, в котором идет обращение к базам данных. Используя его, программист может сосредоточиться на основной логике, сократив написание запросов к минимуму.

Как видите, в приложении Hibernate нет ничего сложно. Главная трудность, с которой вам придется столкнуться — правильная настройка зависимостей и конфигурационных файлов проекта. Также важно правильно пользоваться инструментами для автоматизации сборки проектов: Mavеn, Gradle и другими.

Ссылки с конфигурациями в репозиториях постоянно меняются, поэтому будьте внимательны и, если что-то не работает — проверьте их актуальность.

Стабильность работы приложения на Hibernate во многом зависит от версии Java и используемых модулей (например, Hibernate 6.1.3 с Java 17 вызывает ошибки, а та же версия, но уже с Java 18 работает нормально).

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

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023