Продолжаю цикл статей, посвящённый моему приложению для портфолио с Java Servlet, Hibernate и jsp страницами.
В прошлой части я создал простое Hello World приложение в качестве разминки. В этой статье я начну работать уже непосредственно над своей задачей и с существующей базой данных.
В этой статье буду создавать модели, соответствующие таблицам в базе, DAO слой, добавлять зависимости в pom.xml для работы с Hibernate и базой данных MySQL, и настаивать конфиги подключения.
Опишу для начала, что я хочу сделать.
Задача, может, не самая распространённая в этой форме, но всё же довольно универсальная и на поиграться с приложением самое то. На своём сайте каждую неделю я обновляю личный чарт топ40 на основе данных со своей странички ласт.фм. То есть просто публикую 40 самых прослушиваемых песен каждое воскресенье. Архивы я храню в базе MySQL, но забивать туда данные пока что нужно вручную, и это занимает очень много времени: сначала я составляю список в ворде, потом сохраняю все данные в три таблицы моей базы. На данный момент схема выглядит так:

Приложение должно сделать создание еженедельных чартов и доступ к архивам легче и исключить возможность ошибиться в id или другой колонке. По сути, это будет CRUD-приложение с простой логикой. Что оно должно делать:
- Отображать на главной странице последний чарт.
- Дать доступ к архиву чартов — выбор чарта по дате.
- Показывать историю в чарте для каждого артиста — поиск песен по артисту.
- Показывать историю в чарте для каждой песни — показывать все её позиции в чарте за каждую дату, что она была в чарте.
- Позволять компилировать новый чарт.
Вся работа происходит с уже существующей базой в MySQL, которую я изначально запроектировал не особо правильно, но моим требованиям она отвечает. В базе три таблицы:
- chart с номером чарта и его датой, первичный ключ здесь порядковый номер чарта;
- song с названием песни и её исполнителями, первичный ключ здесь порядковый номер песни в базе;
- position с составным первичным ключом: номер чарта и номер песни.

Зависимости Hibernate и MySQL в pom.xml
Добавим в pom.xml зависимости для работы с Hibernate.
<properties>
<...>
<hibernate.version>5.4.3.Final</hibernate.version>
<org.springframework.version>5.1.4.RELEASE</org.springframework.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<...>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<...>
</dependencies>
Тут же добавлю коннектор к базе данных:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
Создаём модели
Теперь для каждой таблицы в базе создам соответствующий класс. Все связанные с данными классы помещу в пакет data. Модели будут внутри него в ещё одном пакете model.
Перед каждым заголовком класса пишем @Entity и @Table с указанием имени таблицы. Перед каждым полем пишем имя соответствующей колонки в базе с аннотацией @Column и тем же атрибутом name. Перед первичным ключом ставим аннотацию @Id.
Так как мои ключи в базе задаются вручную, оставляю так. Иначе, нужно было бы написать что-то типа этого (но только если в базе на колонке задан auto increment):
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id")
private int id;
Описать таблицы с чартами и песнями будет довольно легко. Просто перечисляю все колонки и задаю геттеры и сеттеры.
@Entity
@Table(name="chart")
public class Chart {
@Id
@Column(name="idChart")
private int id;
@Column(name="Date")
private String date;
public Chart() {
}
public Chart(int id, String date) {
this.id = id;
this.date = date;
}
//тут должны быть геттеры и сеттеры
}
@Entity
@Table(name="song")
public class Song {
@Id
@Column(name="idSong")
private int id;
@Column(name="Weeks")
private int weeks;
@Column(name="Peak")
private int peak;
@Column(name="Artists")
private String artists;
@Column(name="Name")
private String name;
public Song() {
}
public Song(int id, int weeks, int peak, String artists, String name) {
this.id = id;
this.weeks = weeks;
this.peak = peak;
this.artists = artists;
this.name = name;
}
// тут должны быть геттеры и сеттеры
}
С третьей таблицей будет сложнее, потому что в ней есть составной первичный ключ. Я уже писал про такие вещи в контексте Спринга, но на всякий случай повторюсь.
Составной первичный ключ
Итак, есть таблица Position, где я по простоте душевной сделал составной первичный ключ, и теперь должен как-то с этим жить.
Для начала нужно сделать отдельный класс с полями, которые входят в этот ключ и аннотировать его как @Embeddable
@Embeddable
public class Position_PK implements Serializable {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "Chart")
private Chart chart;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idSong")
private Song song;
public Position_PK() {
}
public Position_PK(Chart chart, Song song) {
this.chart = chart;
this.song = song;
}
// тут должны быть геттеры и сеттеры
}
Класс обязательно должен быть Serializable.
Далее создаём модель Position, где вместо ключевых полей указываем созданный только что объект Position_PK с аннотацией @EmbeddedId. Остальные поля описываем по классике.
@Entity
@Table(name="position")
public class Position {
@EmbeddedId
private Position_PK pk;
@Column(name="Position")
private int position;
@Column(name="LastWeek")
private Integer lastWeek;
public Position() {
}
public Position(Position_PK pk, int position, Integer lastWeek) {
this.pk = pk;
this.position = position;
this.lastWeek = lastWeek;
}
// тут должны быть геттеры и сеттеры
}
CRUD операции — слой DAO (Data Access Object)
Теперь пришло время в отдельном пакете для каждой модели создать по классу, который бы работал с этими данными.
Следующий класс предполагает сохранение, получение всех песен из таблицы, получение одной песни по её ключу, обновление и удаление.
public class SongDAOImpl {
public static void save(Song result) {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
session.save(result);
session.getTransaction().commit();
session.close();
}
public static List<Song> getAll() {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
Query query = session.createQuery("FROM Song " +
"ORDER BY id ");
List<Song> list = query.list();
session.getTransaction().commit();
session.close();
return list;
}
public static Song getById(int id) {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
Query query = session.createQuery("FROM Song " +
"WHERE id = :id");
query.setInteger("id", id);
Song result = (Song) query.uniqueResult();
session.getTransaction().commit();
session.close();
return result;
}
public static void update(Song result) {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
session.update(result);
session.getTransaction().commit();
session.close();
}
public static void delete(int id){
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
Query query=session.createQuery("FROM Song " +
"WHERE id = :id");
query.setInteger("id",id);
Song result = (Song) query.uniqueResult();
session.delete(result);
session.getTransaction().commit();
session.close();
}
}
В следующих статьях по надобности буду добавлять другие методы с более сложными манипуляциями с данными. «Более сложными», конечно, громко сказано, но буду соединять несколько таблиц в одном запросе и фильтровать выдачу. Пример фильтрации уже есть выше с помощью привычного WHERE. Нужно понимать, что в кавычках мы пишем не обычный sql, а hql — язык Hibernate. Он похож на sql, но несколько проще.
Посмотрим на примере Position, как соединять таблицы с помощью Join — конструкция намного короче, чем была бы в sql.
public class PositionDAOImpl {
public static void save(Position result) {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
session.save(result);
session.getTransaction().commit();
session.close();
}
public static List<Position> getAll() {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
Query query = session.createQuery("FROM Position p " +
"LEFT JOIN FETCH p.pk.chart " +
"LEFT JOIN FETCH p.pk.song ");
List<Position> list = query.list();
session.getTransaction().commit();
session.close();
return list;
}
public static void update(Position result) {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
session.update(result);
session.getTransaction().commit();
session.close();
}
public static void delete(int id){
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
Query query=session.createQuery("FROM Position " +
"WHERE id = :id");
query.setInteger("id",id);
Position result = (Position) query.uniqueResult();
session.delete(result);
session.getTransaction().commit();
session.close();
}
}
Здесь в методе getAll() я соединяю таблицу с двумя другими, чтобы через объект Position иметь доступ к вложенным в него объектам Chart и Song.
Конфигурация Hibernate
В папке resources создам xml файл с конфигурацией. Обзову его sqlserverMain.cfg.xml
<!DOCTYPE hibernate-configuration SYSTEM
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost:3306/top40_app?useSSL=false</property>
<property name="connection.username">root</property>
<property name="connection.password">root</property>
<property name="connection.pool_size">10000</property>
<property name="hibernate.dbcp.maxActive">-1</property>
<property name="hibernate.dbcp.maxIdle">10</property>
<property name="dialect">org.hibernate.dialect.MySQL8Dialect</property>
<mapping class="com.chart.TopChart.data.model.Song" />
<mapping class="com.chart.TopChart.data.model.Chart" />
<mapping class="com.chart.TopChart.data.model.Position" />
</session-factory>
</hibernate-configuration>
Здесь указываю адрес базы и её название (top40_app), а также данные для авторизации (connection.username и connection.password). У меня оба этих свойства по-простому root (для локальной базы и личного проекта такого уровня безопасности мне хватит).
Также я указал нужный диалект для MySQL 8, но можно использовать и SQLServerDialect по умолчанию.
Далее указываю список всех Entity. Если в таблицу добавляете новую таблицу и создаёте новую модель, не забудьте и здесь добавить ещё одну строчку.
И теперь в пакете util создам класс HibernateUtil:
public class HibernateUtil {
private static SessionFactory sessionFactory = buildSessionFactory();
private static SessionFactory buildSessionFactory() {
try {
return new Configuration().configure("sqlserverMain.cfg.xml").buildSessionFactory();
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}
Можно конечно всю конфигу засунуть в этот класс, но мне проще вынести её в отдельный файл и указывать путь к файлу в качестве аргумента.
Вывод данных из базы
И теперь всё готово, чтобы начать доставать данные из базы. В этой статье ограничусь чтением, а уже дальше приступлю к записи и изменению данных.
Создам сервлет Main:
@WebServlet(name = "main", value = "/")
public class MainServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
List<Song> songs = SongDAOImpl.getAll();
List<Position> positions = PositionDAOImpl.getAll();
request.setAttribute("songs", songs);
request.setAttribute("positions", positions);
request.getRequestDispatcher("main.jsp").forward(request, response);
response.flushBuffer();
}
public void destroy() {
}
}
Здесь я вытаскиваю из базы все песни и все позиции — то есть все данные из двух таблиц и эти списки отправляю на страницу main.jsp.
Перед тем, как взглянуть на main.jsp, нужно добавить в pom.xml ещё одну зависимость:
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
Jakarta Standard Tag Library (ранее известная как JavaServer Pages Standard Tag Library) или просто JSTL позволяет нам перебирать данные в цикле на main.jsp:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "TOP 40!" %>
</h1>
<br/>
<table>
<c:forEach items="${songs}" var="song">
<tr>
<td>${song.name}</td>
<td>${song.artists}</td>
<td>${song.peak}</td>
<td>${song.weeks}</td>
</tr>
</c:forEach>
</table>
<hr>
<hr>
<hr>
<table>
<c:forEach items="${positions}" var="position">
<tr>
<td>${position.pk.chart.id}</td>
<td>${position.pk.chart.date}</td>
<td>${position.position}</td>
<td>${position.lastWeek}</td>
<td>${position.pk.song.name}</td>
<td>${position.pk.song.artists}</td>
</tr>
</c:forEach>
</table>
</body>
</html>
В цикле forEach я перебираю все объекты из списков songs и positions.
В случае с песнями для каждой song можно вытащить все поля этого объекта, обращаясь к ним через точку (${song.name}).
С позициями обращения будут длиннее, но так как в DAO я заджойнил таблицы, никаких проблем это тоже не должно вызвать. Так как chart и song являются частью составного первичного ключа, нам нужно сначала обратиться к ключу, а потом достать из него нужный объект. Получается ${position.pk.chart.date} для поля date объекта Chart (который в ключе обозван chart) и ${position.pk.song.artists} для поля artists объекта Song (в ключе он song).
На этом всё. Получилось немного длинно, но зато в этой статье я сделал кучу полезных вещей: создал модели и DAO слой, настроил Hibernate и подключение к базе данных, а также подготовил всё для работы с данными и даже написал первый сервлет, который пока выглядит не особо симпатично (просто в таблице без стилей показывает содержимое двух таблиц)

В следующих статьях из цикла превращу главную страницу в более адекватную и буду выводить там лишь последний чарт, добавлю архив (поиск чарта по дате или номеру, поиск песен по артисту и отображение истории каждой песни) и возможность собирать и сохранять новый чарт (поработаю с doPost и формами и может быть ajax — ещё не решил, как это всё будет выглядеть). Подготовка проекта закончена.
5 Comments