Добридень. У цій статті я хотів би поділитися своїм першим знайомством з такими речами як Maven, Spring, Hibernate, MySQL та Tomcat у процесі створення простого CRUD програми. Це третя частина з чотирьох. іншими незнайомими словами. Це третя частина статті "Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток". Попередні частини можна побачити, перейшовши за посиланнями:
- Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 1)
- Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 2)
Зміст:
- Створення та підключення бази даних
- ORM та JPA
- Entity
- Властивості Hibernate
- Конфігурація Hibernate
- Шар доступу до даних
Створення та підключення бази даних
Ну що ж, настав час зайнятися базою даних. Перш ніж підключати Hibernate і думати як воно все повинно там працювати, спочатку розберемося з базою даних, тобто. створимо її, підключимо, зробимо та заповнимо табличку. Використовувати ми будемо СУБД (Система Управління Базами Даних) MySQL (зрозуміло потрібно спочатку завантажити та встановити). SQL (Structured Query Language – мова структурованих запитів) – декларативна мова програмування, що використовується для створення, модифікації та управління даними в реляційній базі даних. У таких базах дані зберігаються як таблиць. Яким чином додаток спілкується з базою даних (передача SQL запитів у БД та повернення результатів). Для цього Java має таку штуку як JDBC (Java DataBase Connectivity), Яка, попросту кажучи, являє собою набір інтерфейсів і класів для роботи з базами даних. Щоб взаємодіяти з БД необхідно створити з'єднання, для цього в пакетіjava.sql
є клас Connection
. Існує кілька способів встановлення з'єднання, наприклад, можна використовувати метод getConnection
класу DriverManager
. Однак взаємодія з БД здійснюється не безпосередньо, адже баз даних багато, і вони є різними. Так що для кожної з них існує свій JDBC Driver За допомогою цього драйвера і встановлюється з'єднання з базою. Тому перш за все, щоб потім на це не відволікатися, встановимо драйвер MySQL . Додамо до pom.xml
наступної залежності:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
Тепер створимо базу даних. View -> Tool Windows -> Database - відкриється панель бази даних. New (зелений +) -> Data Source -> MySQL - відкриється вікно, в якому потрібно вказати ім'я користувача та пароль, ми задавали їх при встановленні MySQL (для прикладу я використовував root і root). Порт (для MySQL за промовчанням 3306), ім'я і т.д. залишаємо як є. Можна перевірити з'єднання кнопкою " Test Connection ". Тиснемо ОК і ось ми підключабося до сервера MySQL. Далі створимо базу даних. Для цього можна в консолі, що відкрилася, написати скрипт:
CREATE DATABASE test
Тиснемо Execute і база даних готова, тепер її можна підключити, для цього повертаємося в Data Source Properties і в поле Database вводимо ім'я бази (test), потім знову вводимо ім'я користувача з паролем і тиснемо ОК. Тепер потрібно зробити таблицю. Можна використовувати графічні інструменти, але для першого разу, мабуть, варто написати ручками скрипт, подивитися хоч як воно виглядає:
USE test;
CREATE TABLE films
(
id int(10) PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
year int(4),
genre VARCHAR(20),
watched BIT DEFAULT false NOT NULL
)
COLLATE='utf8_general_ci';
CREATE UNIQUE INDEX films_title_uindex ON films (title);
INSERT INTO `films` (`title`,`year`,`genre`, watched)
VALUES
("Inception", 2010, "sci-fi", 1),
("The Lord of the Rings: The Fellowship of the Ring", 2001, "fantasy", 1),
("Tag", 2018, "comedy", 0),
("Gunfight at the O.K. Corral", 1957, "western", 0),
("Die Hard", 1988, "action", 1);
Створюється таблиця з назвою films
зі стовпцями id
і title
т.д. Для кожного шпальти вказується тип (у дужках максимальний розмір виведення).
PRIMARY KEY
- Це первинний ключ, служить для однозначної ідентифікації запису в таблиці (що має на увазі унікальність)AUTO_INCREMENT
— значення буде генеруватися автоматично (саме воно буде ненульовим, так що це можна не вказувати)NOT NULL
- тут теж все очевидно, не може бути порожнімDEFAULT
— встановити значення за замовчуваннямCOLLATE
- кодуванняCREATE UNIQUE INDEX
- зробити поле унікальнимINSERT INTO
— додати запис до таблиці
main
, тимчасово. Його, в принципі, куди завгодно можна засунути, хоч у клас контролера, хоч моделі чи конфігурації, не має значення, потрібно просто переконатися з його допомогою що все нормально зі з'єднанням і можна його видаляти. Але щоб було акуратніше створимо для нього окремий клас Main
:
package testgroup.filmography;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Main {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
System.out.println("Connecting...");
try (Connection connection = DriverManager.getConnection(url, username, password)) {
System.out.println("Connection successful!");
} catch (SQLException e) {
System.out.println("Connection failed!");
e.printStackTrace();
}
}
}
Тут все просто, задаємо параметри підключення до нашої БД та намагаємося створити з'єднання. Запускаємо цейmain
і дивимось. Отже, у мене вискочив виняток, якісь проблеми з часовим поясом і ще якесь попередження щодо SSL. Погулявши просторами інтернету можна з'ясувати, що це досить поширена проблема, причому при використанні різних версій драйвера (mysql-connector-java) може сваритися по-різному. Наприклад, досвідченим шляхом я з'ясував, що при використанні версії 5.1.47 винятків через часовий пояс немає, з'єднання нормально створюється, але все одно висякує попередження SSL. Ще з якимись версіями начебто було, що й з приводу SSL був виняток, а не просто попередження. Ну гаразд, не суть. Можна окремо спробувати розібратися з цим питанням, але зараз у це не поглиблюватимемося. Вирішується це все досить просто, потрібно вказати в URL додаткові параметри, а самеserverTimezone
, якщо проблема з часовим поясом, і useSSL
, якщо проблема з SSL:
String url = "jdbc:mysql://localhost:3306/test?serverTimezone=Europe/Minsk&useSSL=false";
Тепер ми задали часовий пояс і відключабо SSL. Знову запускаємо main
і вуаля – Connection successful! Ну що ж, добре, як створювати з'єднання розібралися. Клас Main
своє завдання у принципі виконав, можна його видаляти.
ORM та JPA
По-хорошому, для кращого розуміння, починати ознайомлення з базами даних краще по порядку, від початку, без будь-яких там гібернейтів та іншого. Тому тут не зайвим буде знайти якісь гайди і спочатку спробувати попрацювати за допомогою класів JDBC, ручками писати SQL-запити ну і таке інше. Ну а тут мабуть одразу перейдемо до ORM моделі. Що це означає. Про це звичайно знову ж таки бажано почитати окремо, але я спробую коротко описати. ORM(Object-Relational Mapping або об'єктно-реляційне відображення) - це технологія для відображення об'єктів у структурі реляційних баз даних, тобто. щоб представити наш джава-об'єкт у вигляді рядка таблиці. Завдяки ORM можна не дбати про написання SQL-скриптів і зосередитися на роботі з об'єктами. Як цим скористатися. Java має ще одну чудову штуку, JPA (Java Persistence API), яка реалізує ORM концепцію. JPA - це така специфікація, вона визначає вимоги до об'єктів, у ній визначені різні інтерфейси та інструкції для роботи з БД. JPA є власне описом, стандартом. Тому є безліч конкретних реалізацій, однією з яких (причому однією з найпопулярніших) є Hibernate, в цьому, власне, і полягає суть цього фреймворку. Hibernate - реалізація специфікації JPA, призначена для розв'язання задач об'єктно-реляційного відображення (ORM). Потрібно підключити всю цю справу до нашого проекту. Крім того, для того, щоб наш Spring не стояв собі осторонь і теж брав участь у всій цій движусі з базами даних, необхідно підключити ще пару модулів, т.к. все, що ми отримали від залежностіspring-webmvc для цього вже мало. Нам ще знадобиться spring-jdbc , для роботи з базою даних, spring-tx , для підтримки транзакцій, та spring-orm , для роботи з Hibernate. Додамо залежності вpom.xml
:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.3.7.Final</version>
</dependency>
Достатньо цих двох залежностей. javax.persistence-api
під'їде разом з hibernate-core , а spring-jdbc і spring-tx разом зі spring-orm .
Entity
Отже, хочемо, щоб об'єкти класуFilm
могли бути збережені у базі даних. Для цього клас повинен задовольняти низку умов. У JPA при цьому є таке поняття як Сутність (Entity) . Клас-сутність це звичайний POJO клас, з приватними полями та гетерами та сеттерами для них. У нього обов'язково має бути не приватний конструктор без параметрів (або конструктор за замовчуванням), і він повинен мати первинний ключ, тобто. що однозначно ідентифікувати кожен запис цього класу в БД. Про всі вимоги до такого класу можна почитати окремо. Зробимо наш клас Film
сутністю за допомогою JPA анотацій:
package testgroup.filmography.model;
import javax.persistence.*;
@Entity
@Table(name = "films")
public class Film {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "title")
private String title;
@Column(name = "year")
private int year;
@Column(name = "genre")
private String genre;
@Column(name = "watched")
private boolean watched;
// + getters and setters
}
@Entity
- Вказує на те, що даний клас є сутністю.@Table
- Вказує на конкретну таблицю для відображення цієї сутності.@Id
- Вказує, що це поле є первинним ключем, тобто. ця властивість буде використовуватися для ідентифікації кожного унікального запису.@Column
- пов'язує поле зі стовпцем таблиці. Якщо імена поля та стовпця таблиці збігаються, можна не вказувати.@GeneratedValue
- Властивість буде генеруватися автоматично, в дужках можна вказати яким чином. Не будемо зараз розбиратися, як саме працюють різні стратегії. Досить знати, що в даному випадку кожне нове значення збільшуватиметься на 1 від попереднього.
Властивості Hibernate
Ну що ж, приступимо до налаштування нашого Hibernate. І насамперед винесемо деяку інформацію, таку як ім'я користувача та пароль, url і ще дещо в окремий файл. Можна звичайно вказувати їх звичайним рядком прямо в класі, як ми робабо це, коли перевіряли з'єднання (String username = "root";
і потім передавали це в метод для створення з'єднання). Але все ж таки правильніше зберігати такі статичні дані в якому-небудь property
файлі. І якщо, наприклад, потрібно змінити базу даних, тоді не доведеться лазити по всіх класах і шукати де це використовується, достатньо буде один раз змінити значення в цьому файлі. Створимо файл db.properties у директорії resources :
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=Europe/Minsk&useSSL=false
jdbc.username=root
jdbc.password=root
hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
hibernate.show_sql=true
Ну згори все зрозуміло, параметри підключення до бд, тобто. ім'я класу драйвера, урл, ім'я користувача та пароль. hibernate.dialect
— ця властивість потрібна для того, щоб вказати Hibernate, який саме варіант мови SQL використовується. Справа в тому, що в кожній СУБД для того, щоб розширити можливості, додати якийсь функціонал чи щось оптимізувати, зазвичай трохи модернізують мову. Через війну виходить, що з кожної СУБД свій SQL діалект. Це як з англійською, наче мова одна, але в Австралії, США чи Британії вона буде трохи відрізнятися, і якісь слова можуть мати різне значення. І для того, щоб не було жодних проблем з розумінням, потрібно прямо повідомити Hibernate з чим саме він має мати справу. hibernate.show_sql
— завдяки цій властивості консолі відображатимуться запити до БД. Це не обов'язково, але з цією штукою хоч можна глянути, що відбувається, а то інакше може здатися, що Hibernate якусь магію творить. Ну, воно звичайно не зовсім зрозуміло буде виводити, краще для цього якийсь логер використовувати, але це якось іншим разом, поки і так зійде.
Конфігурація Hibernate
Перейдемо до конфігурації. Створимо в пакетіconfig
клас HibernateConfig
:
package testgroup.filmography.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@ComponentScan(basePackages = " testgroup.filmography")
@EnableTransactionManagement
@PropertySource(value = "classpath:db.properties")
public class HibernateConfig {
private Environment environment;
@Autowired
public void setEnvironment(Environment environment) {
this.environment = environment;
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect"));
properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql"));
return properties;
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName"));
dataSource.setUrl(environment.getRequiredProperty("jdbc.url"));
dataSource.setUsername(environment.getRequiredProperty("jdbc.username"));
dataSource.setPassword(environment.getRequiredProperty("jdbc.password"));
return dataSource;
}
@Bean
public LocalSessionFactoryBean sessionFactory() {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
sessionFactory.setPackagesToScan("testgroup.filmography.model");
sessionFactory.setHibernateProperties(hibernateProperties());
return sessionFactory;
}
@Bean
public HibernateTransactionManager transactionManager() {
HibernateTransactionManager transactionManager = new HibernateTransactionManager();
transactionManager.setSessionFactory(sessionFactory().getObject());
return transactionManager;
}
}
Тут досить багато нового, тому краще за кожним пунктом додатково пошукати інформацію в різних джерелах. Тут же коротко пройдемося.
- З
@Configuration
і@ComponentScan
вже розібралися коли робабо класWebConfig
. @EnableTransactionManagement
- дозволяє використовуватиTransactionManager
для керування транзакціями. Hibernate працює з БД з допомогою транзакцій, вони необхідні щоб якийсь набір операцій виконувався як єдине ціле, тобто. якщо в методі виникнуть проблеми з якоюсь однією операцією, тоді не виконаються і всі інші, щоб не було як у класичному прикладі з переказом грошей, коли операція зняття грошей з одного рахунку відбулася, а операція запису на інший не спрацювала, в результаті гроші зникли.@PropertySource
- Підключення файлу властивостей, який ми нещодавно створювали.Environment
— щоб отримати властивості зproperty
файлу.hibernateProperties
— цей метод потрібен, щоб представити властивості Hibernate у вигляді об'єкта PropertiesDataSource
використовується для створення з'єднання з БД. Це альтернатива DriverManager , яку ми використовували раніше, коли створювали для перевірки підключення методmain
. У документації сказано, щоDataSource
використовувати краще. Так і вчинимо, звичайно не забувши почитати в інтернеті в чому різниця і переваги. Зокрема, однією з переваг є можливість створення пулу з'єднань Database Connection Pool (DBCP).sessionFactory
- Для створення сесій, за допомогою яких здійснюються операції з об'єктами-сутностями. Тут ми встановлюємо джерело даних, властивості Hibernate і в якому пакеті потрібно шукати класи-сутності.transactionManager
- Для налаштування менеджера транзакцій.
DataSource
. У документації сказано, що використовувати стандартну реалізацію, зокрема DriverManagerDataSource
, не рекомендується, т.к. це лише заміна нормального пулу з'єднань і загалом підходить тільки для тестів та всього такого. Для нормальної програми краще використовувати якусь DBCP бібліотеку. Ну, для нашого застосування звичайно вистачить і того, що є, але для повноти картини, мабуть, все-таки використовуємо іншу реалізацію, як радять. Додамо до pom.xml
наступної залежності:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.10</version>
</dependency>
І в методі dataSource
класу HibernateConfig
замінимо DriverManagerDataSource
на BasicDataSource
з пакета org.apache.tomcat.dbcp.dbcp2
:
BasicDataSource dataSource = new BasicDataSource();
Ну начебто все, конфігурація готова, залишилося додати її до нашого AppInitializer :
protected Class<?>[] getRootConfigClasses() {
return new Class[]{HibernateConfig.class};
}
Шар доступу до даних
Настав час зайнятися нарешті нашим DAO. Переходимо до класуFilmDAOImpl
і насамперед видаляємо звідти пробний список, він нам більше не потрібен. Додаємо фабрику сесій і працюватимемо через неї.
private SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
Для початку зробимо метод для відображення сторінки зі списком фільмів, в ньому ми будемо отримувати сесію та робити запит до бд (витягувати усі записи та формувати список):
public List<Film> allFilms() {
Session session = sessionFactory.getCurrentSession();
return session.createQuery("from Film").list();
}
Тут є два моменти. По-перше, висвітлилося попередження. Це з тим, що хочемо отримати параметризований List<Film>
, але метод повертає просто List
, оскільки під час компіляції невідомо який тип поверне запит. Отже, ідея нас попереджає, що ми робимо небезпечне перетворення, внаслідок чого можуть виникнути неприємності. Є кілька правильніших способів як це зробити, щоб такого питання не виникало. Можна знайти інформацію в інтернеті. Але зараз не будемо з цим морочитися. Справа в тому, що ми точно знаємо який тип буде повернутий, так що ніяких проблем тут не виникне, можна просто не звертати на попередження увагу. Але, щоб очі не мозолило, можна повісити над способом інструкцію@SupressWarning("unchecked")
. Цим ми як би скажемо компілятору, мовляв, дякую, друже, за занепокоєння, але я знаю, що роблю і тримаю все під контролем, тому можеш розслабитися і не турбуватися на рахунок цього методу. По-друге, ідея підкреслює червоним" from Film
. Просто це HQL (Hibernate Query Language) запит та ідея не розуміє, правильно там все чи є помилка. Можна зайти в налаштування ідеї та вручну все відрегулювати (шукаємо в інтернеті, якщо цікаво). Або можна просто додати підтримку Hibernate фреймворку, для цього тиснемо правою кнопкою за проектом, вибираємо Add Framework Support , ставимо галочку для Hibernate і тиснемо ОК. Після цього швидше за все в класі-сутності ( Film
) теж багато всього підкреслить червоним, наприклад там, де анотація @Table(name = "films")
видасть попередженняCannot resolve table 'films' . Тут знову ж таки нічого страшного, це не помилка проекту, все скомпілюється і працюватиме. Ідея підкреслює, бо нічого не знає про нашу базу. Щоб це виправити, зробимо інтеграцію ідеї з БД. View -> Tool Windows -> Persistense (відкриється вкладка) -> права кнопка миші вибираємо Assign Data Sources -> у Data Source вказуємо з'єднання з БД і тиснемо ОК . Коли це все виправабо, залишилося ще дещо. Переходимо до рівня вище, у сервіс. У класі FilmServiceImpl
позначаємо метод allFilms
spring анотацією@Transactional
яка вкаже на те, що метод повинен виконуватися в транзакції (без цього Hibernate працювати відмовиться):
@Transactional
public List<Film> allFilms() {
return filmDAO.allFilms();
}
Так, тут все готово, в контролері начебто нічого чіпати не треба. Ну що ж, схоже настав момент істини, тиснемо Run і дивимося, що вийде. І ось вона наша табличка, і цього разу отримана не зі списку, який ми самі зробабо прямо в класі, а з бази даних. Чудово, схоже, все працює. Тепер таким же чином робимо всі інші CRUD операції за допомогою способів сесії. У результаті клас виглядає так:
package testgroup.filmography.dao;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import testgroup.filmography.model.Film;
import java.util.List;
@Repository
public class FilmDAOImpl implements FilmDAO {
private SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
@SuppressWarnings("unchecked")
public List<Film> allFilms() {
Session session = sessionFactory.getCurrentSession();
return session.createQuery("from Film").list();
}
@Override
public void add(Film film) {
Session session = sessionFactory.getCurrentSession();
session.persist(film);
}
@Override
public void delete(Film film) {
Session session = sessionFactory.getCurrentSession();
session.delete(film);
}
@Override
public void edit(Film film) {
Session session = sessionFactory.getCurrentSession();
session.update(film);
}
@Override
public Film getById(int id) {
Session session = sessionFactory.getCurrentSession();
return session.get(Film.class, id);
}
}
Тепер залишилося тільки не забути зайти в сервіс та повісити на методи інструкцію @Transactional
. Ось і все готове. Можна тепер запустити та перевірити. Натискати посилання та кнопки, спробувати додати/видалити/змінити записи. Якщо все зроблено правильно має працювати. Тепер це вже повноцінний CRUD додаток з використаними Hibernate, Spring, MySQL. Далі буде... Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 1) Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 2) Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 3) Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 4)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ