JavaRush /Java блог /Random UA /Знайомство з Maven, Spring, MySQL, Hibernate та перший CR...
Макс
41 рівень

Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 3)

Стаття з групи Random UA
Добридень. У цій статті я хотів би поділитися своїм першим знайомством з такими речами як Maven, Spring, Hibernate, MySQL та Tomcat у процесі створення простого CRUD програми. Це третя частина з чотирьох. іншими незнайомими словами. Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 3) - 1Це третя частина статті "Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток". Попередні частини можна побачити, перейшовши за посиланнями:

Зміст:

Створення та підключення бази даних

Ну що ж, настав час зайнятися базою даних. Перш ніж підключати 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 ". Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 3) - 2Тиснемо ОК і ось ми підключабося до сервера 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— додати запис до таблиці
В результаті вийшла така табличка: Мабуть, варто спробувати підключитися до неї, поки що просто так, окремо від нашого веб-додатку. Раптом якісь косяки виникнуть із цим, тоді одразу розберемося. А то пізніше Hibernate підключатимемо, робити щось, налаштовуватимемо, колупатимемо, і якщо там десь накосячим, то хоч будемо знати що проблема не тут. Ну і щоб перевірити з'єднання зробимо спосіб 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 застосовувати інструкції рекомендується не до полів, а до гетер. Однак різниця між цими підходами досить тонка і в нашому додатку це ніяк не вплине. Крім того, більшість людей все одно ставлять інструкції над полями. Тому, залишимо так, виглядає обережніше.

Властивості 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 у вигляді об'єкта Properties
  • DataSourceвикористовується для створення з'єднання з БД. Це альтернатива 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 вказуємо з'єднання з БД і тиснемо ОК . Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 3) - 3Коли це все виправабо, залишилося ще дещо. Переходимо до рівня вище, у сервіс. У класі FilmServiceImplпозначаємо метод allFilmsspring анотацією@Transactionalяка вкаже на те, що метод повинен виконуватися в транзакції (без цього Hibernate працювати відмовиться):
@Transactional
public List<Film> allFilms() {
    return filmDAO.allFilms();
}
Так, тут все готово, в контролері начебто нічого чіпати не треба. Ну що ж, схоже настав момент істини, тиснемо Run і дивимося, що вийде. Знайомство з Maven, Spring, MySQL, Hibernate та перший CRUD додаток (частина 3) - 4І ось вона наша табличка, і цього разу отримана не зі списку, який ми самі зробабо прямо в класі, а з бази даних. Чудово, схоже, все працює. Тепер таким же чином робимо всі інші 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)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ