Hibernate — засіб відображення між об'єктами та реляційними структурами (object-relational mapping, ORM) для платформи Java. Hibernate є вільним програмним забезпеченням, яке поширюється на умовах GNU Lesser General Public License. Hibernate надає легкий для використання каркас (фреймворк) для відображення між об'єктно-орієнтованою моделлю даних і традиційною реляційною базою даних.
У цій статті будуть розкриті основи використання Java ORM-бібліотеки Hibernate 3-й версії на невеликому прикладі.
В якості середовища розробки використовувався NetBeans версії 7.1. Для початку створіть Maven-проект в NetBeans:
File -> New Project... -> Maven -> Java Application
Додамо залежності в проект. Повний файл pom.xml показаний нижче:
<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.seostella.hibernate</groupId>
<artifactId>hibernate_basics</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hibernate_basics</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>jboss</id>
<url>http://repository.jboss.org/maven2</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>3.6.9.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>3.5.6-Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>3.1.0.GA</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-tools</artifactId>
<version>3.2.4.GA</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Також нам знадобиться конфігураційний файл hibernate:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration SYSTEM "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/hibernate_basics</property>
<property name="hibernate.connection.username">hb_user</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
<property name="connection.password">hb_password</property>
<!-- <property name="show_sql">true</property>-->
<!-- <mapping jar="hibernate-mappings.jar"/> -->
<mapping class="com.seostella.hibernate.basics.entity.Article"/>
<mapping class="com.seostella.hibernate.basics.entity.Category"/>
<mapping class="com.seostella.hibernate.basics.entity.User"/>
</session-factory>
</hibernate-configuration>
У даному прикладі для доступу до бази даних hibernate_basics використовується користувач hb_user з паролем hb_password. Змініть ці параметри на ті, що використовуються Вами.
Дамп використовуваної бази представлений нижче:
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
CREATE TABLE IF NOT EXISTS `article` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8 NOT NULL,
`message` varchar(255) CHARACTER SET utf8 NOT NULL,
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
INSERT INTO `article` (`id`, `title`, `message`, `user_id`) VALUES
(1, '"Законы" экологии Коммонера', 'К. обращает внимание на всеобщуюсвязь процессов и явлений в природе и близок по смыслу к закону внутреннегодинамического равновесия изменение одного из показателей системы вызываетфункционально-структурные количественные и качественные перемены, при этом ', 3),
(2, '"Прикладная" экология', 'Смысл изучения даннойнауки заключается в получении знания, как сохранить наш дом чистым и пригоднымдля обитания в течение долгих лет. Поскольку целью образования являются не знания, а действия Герберт Спенсер , то анализсуществующего экологического положе', 3);
CREATE TABLE IF NOT EXISTS `category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `title` (`title`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;
INSERT INTO `category` (`id`, `title`) VALUES
(1, 'Биология');
CREATE TABLE IF NOT EXISTS `category_article` (
`categories_id` bigint(20) NOT NULL,
`articles_id` bigint(20) NOT NULL,
PRIMARY KEY (`categories_id`,`articles_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
INSERT INTO `category_article` (`categories_id`, `articles_id`) VALUES
(1, 1),
(1, 2);
CREATE TABLE IF NOT EXISTS `hb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ;
INSERT INTO `hb_user` (`id`, `name`, `password`) VALUES
(3, 'John Connor', '1');
Структура проекту, в тому числі, де розташовується конфігураційний файл hibernate.cfg.xml показана на Рис.1.
Рис 1. Структура проекту
Приступимо до реалізації взаємодії з базою. Почнемо зі створення базового класу для роботи з Hibernate і, відповідно, з базою даних. Створимо в пакеті com.seostella.hibernate.basics.dao клас DAO:
package com.seostella.hibernate.basics.dao;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
/**
*
* @author seostella.com
*/
public class DAO {
private static final Logger log = Logger.getAnonymousLogger();
private static final ThreadLocal session = new ThreadLocal();
private static final SessionFactory sessionFactory =
new AnnotationConfiguration().configure().buildSessionFactory();
protected DAO() {
}
public static Session getSession() {
Session session = (Session) DAO.session.get();
if (session == null) {
session = sessionFactory.openSession();
DAO.session.set(session);
}
return session;
}
protected void begin() {
getSession().beginTransaction();
}
protected void commit() {
getSession().getTransaction().commit();
}
protected void rollback() {
try {
getSession().getTransaction().rollback();
} catch (HibernateException e) {
log.log(Level.WARNING, "Cannot rollback", e);
}
try {
getSession().close();
} catch (HibernateException e) {
log.log(Level.WARNING, "Cannot close", e);
}
DAO.session.set(null);
}
public static void close() {
getSession().close();
DAO.session.set(null);
}
}
Цей клас містить базові методи для роботи з Hibernate: getSession() - одержання сесії, begin(), commit(), rollback() - початок, комит і відкат транзакції відповідно, close() - закриття сесії.
Увага! Об'єкт SessionFactory є "важким" - на його створення йде багато часу. Тому намагайтеся цей об'єкт ініціалізувати тільки раз, або як можна рідше.
Приступимо до створення об'єктів. Почнемо з класу для роботи з об'єктом User:
package com.seostella.hibernate.basics.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
/**
*
* @author seostella.com
*/
@Entity
@Table(name="hb_user")
public class User {
private long id;
private String name;
private String password;
public User() {
}
public User(String name, String password) {
this.name = name;
this.password = password;
}
@Column(unique=true)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Id
@GeneratedValue
protected long getId() {
return id;
}
protected void setId(long id) {
this.id = id;
}
}
Анотація @Entity використовується для того, щоб повідомити Hibernate, що клас взаємодіє з ним. Також необхідно повідомити про те, що додався маппінг-файл, додавши в конфігураційний файл hibernate.cfg.xml рядок "mapping class = ...". Приклад:
<hibernate-configuration>
<session-factory>
...
<mapping class="com.seostella.hibernate.basics.entity.Article"/>
...
</session-factory>
</hibernate-configuration>
Якщо у Вас є jar-файл, що складаються з класів, позначених як @Entity, Ви можете додати його одним рядком:
<hibernate-configuration>
<session-factory>
...
<mapping jar="hibernate-mappings.jar"/>
...
</session-factory>
</hibernate-configuration>
Розглянемо інші анотації, які використовувалися в цьому класі, а саме: @Table, @Column, @Id, @GeneratedValue.
@Table - анотація, яка використовується для явного зазначення назви таблиці. Так як база даних містить таблицю "hb_user", а не просто "user", була використана анотація @Table (name = "hb_user") щоб явно вказати назву таблиці.
@Column - використовувалася в прикладі для вказівки унікальності імені користувача наступним чином:
@Column(unique=true)
public String getName() {
return name;
}
@Id - анотація використовується для вказівки Primary-ключа;
@GeneratedValue - повідомляє Hibernate, що значення має генеруватися автоматично при додаванні нового об'єкта в базу.
Останні дві анотації використовуються в прикладі разом з полем id:
@Id
@GeneratedValue
protected long getId() {
return id;
}
Тепер створюємо DAO-клас для роботи з об'єктами User:
package com.seostella.hibernate.basics.dao;
import com.seostella.hibernate.basics.entity.User;
import org.hibernate.HibernateException;
import org.hibernate.Query;
/**
*
* @author seostella.com
*/
public class UserDAO extends DAO {
public User createUser(String username, String password)
throws Exception {
try {
begin();
User user = new User(username, password);
getSession().save(user);
commit();
return user;
} catch (HibernateException e) {
rollback();
throw new Exception("Could not create user " + username, e);
}
}
public User retrieveUser(String username) throws Exception {
try {
begin();
Query q = getSession().createQuery("from User where name = :username");
q.setString("username", username);
User user = (User) q.uniqueResult();
commit();
return user;
} catch (HibernateException e) {
rollback();
throw new Exception("Could not get user " + username, e);
}
}
public void deleteUser( User user ) throws Exception {
try {
begin();
getSession().delete(user);
commit();
} catch (HibernateException e) {
rollback();
throw new Exception("Could not delete user " + user.getName(), e);
}
}
}
Як видно з прикладу, все досить просто: для збереження, пошуку і видалення користувача використовуються такі відповідні рядкикода:
getSession().save(user); // збереження
getSession().createQuery("...").uniqueResult(); // пошук
getSession().delete(user); // видалення
Для забезпечення цілісності бази та для захисту від помилок код обрамляється наступним чином:
try {
begin();
// какой-то код для работы с базой данных
commit();
} catch (HibernateException e) {
rollback();
throw new Exception("Could not create user " + username, e);
}
Створимо головний клас UserApp для демонстрації вище наведеного коду:
package com.seostella.hibernate.basics;
import com.seostella.hibernate.basics.dao.UserDAO;
import com.seostella.hibernate.basics.entity.User;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.logging.Logger;
/**
*
* @author seostella.com
*/
public class UserApp {
private static Logger logger = Logger.getLogger(UserApp.class.getName());
public static void main(String[] args) {
String userInput = ""; // Line read from standard in
InputStreamReader converter = new InputStreamReader(System.in);
BufferedReader in = new BufferedReader(converter);
String username = "";
try {
while (!(userInput.equals("0"))) {
System.out.println("1. Создать пользователя");
System.out.println("2. Найти пользователя");
System.out.println("3. Удалить пользователя");
System.out.println("0. Выход");
userInput = in.readLine();
if ("1".equals(userInput)) {
try {
System.out.print(" Введите имя пользователя: ");
username = in.readLine();
UserDAO userDAO = new UserDAO();
User user = userDAO.createUser(username, "1");
System.out.println("Пользователь создан. Имя: "
+ user.getName() + " пароль: " + user.getPassword());
} catch (Exception e) {
System.out.println("Пользователь " + username + " уже существует.");
}
} else if ("2".equals(userInput)) {
try {
System.out.print(" Введите имя пользователя: ");
username = in.readLine();
UserDAO userDAO = new UserDAO();
User user = userDAO.retrieveUser( username );
System.out.println( "Пользователь получен из базы данных. Имя: "
+ user.getName() + " пароль: " + user.getPassword());
} catch (Exception e) {
System.out.println("Пользователь " + username + " не существует.");
}
} else if( "3".equals( userInput ) ){
try {
System.out.print(" Введите имя пользователя: ");
username = in.readLine();
UserDAO userDAO = new UserDAO();
User user = userDAO.retrieveUser( username );
userDAO.deleteUser( user );
System.out.println( "Пользователь " + username + " удален из базы данных.");
} catch (Exception e) {
System.out.println("Пользователь " + username + " не существует.");
}
}
}
} catch (Exception e) {
}
}
}
Ви можете запустити цей клас на виконання і, використовуючи клавіші "1", "2" і "3", додати, знайти і видалити користувача відповідно. Приклад роботи програми:
1. Создать пользователя
2. Найти пользователя
3. Удалить пользователя
0. Выход
1
Введите имя пользователя: Homer
Пользователь создан. Имя: Homer пароль: 1
1. Создать пользователя
2. Найти пользователя
3. Удалить пользователя
0. Выход
2
Введите имя пользователя: Homer
Пользователь получен из базы данных. Имя: Homer пароль: 1
1. Создать пользователя
2. Найти пользователя
3. Удалить пользователя
0. Выход
3
Введите имя пользователя: Homer
Пользователь Homer удален из базы данных.
1. Создать пользователя
2. Найти пользователя
3. Удалить пользователя
0. Выход
2
Введите имя пользователя: Homer
Пользователь Homer не существует.
1. Создать пользователя
2. Найти пользователя
3. Удалить пользователя
0. Выход
0
Аналогічним способом (по відношенню до класу User) створюємо класи Article і Category:
package com.seostella.hibernate.basics.entity;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
/**
*
* @author seostella.com
*/
@Entity
public class Article {
private long id;
private String title;
private String message;
private User user;
private Set<Category> categories;
public Article(String title, String message, User user) {
this.title = title;
this.message = message;
this.user = user;
this.categories = new HashSet<Category>();
}
public Article() {
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@ManyToOne
@JoinColumn(name = "user_id")
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@ManyToMany(mappedBy = "articles")
public Set<Category> getCategories() {
return categories;
}
public void setCategories(Set<Category> categories) {
this.categories = categories;
}
@Id
@GeneratedValue
protected long getId() {
return id;
}
protected void setId(long id) {
this.id = id;
}
}
package com.seostella.hibernate.basics.entity;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
/**
*
* @author seostella.com
*/
@Entity
public class Category {
private long id;
private String title;
private Set<Article> articles = new HashSet<Article>();
public Category() {
}
public Category(String title) {
this.title = title;
this.articles = new HashSet<Article>();
}
@ManyToMany
@JoinTable(name="category_article")
public Set<Article> getArticles() {
return articles;
}
void setArticles(Set<Article> adverts) {
this.articles = adverts;
}
public void addArticles(Article advert) {
getArticles().add(advert);
}
@Column(unique=true)
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Id
@GeneratedValue
protected long getId() {
return id;
}
protected void setId(long id) {
this.id = id;
}
}
Розглянемо нові анотації @ManyToOne, @JoinColumn, @ManyToMany и @JoinTable:
@ManyToOne - анотація застосовується до поля якщо таблиця пов'язана з іншою таблицею типом один-до-багатьох;
@JoinColumn - за допомогою цієї анотації вказується назва поля таблиці, по якому відбувається зв'язок один-до-багатьох з іншою таблицею;
У нашому випадку таблиці user та article пов'язані між собою зв'язком один-до-багатьох полем user_id таблиці article. Тому в класі Article поле user_id описано наступним способом:
@ManyToOne
@JoinColumn(name = "user_id")
public User getUser() {
return user;
}
Як бачимо, Hibernate спрощує життя, відразу ж повертаючи об'єкт User, замість лише одного ідентифікатора користувача.
У нашому прикладі також є зв'язок багато-до-багатьох між таблицями article і category через проміжну таблицю category_article. Щоб вказати цей зв'язок в Hibernate необхідно скористатися анотаціями @ManyToMany і @JoinTable. В одному з класів Article або Category необхідно прописати інструкцію виду @ManyToMany(mappedBy = "...") , а в іншому:
@ManyToMany
@JoinTable(name="...")
У прикладі використовувалися такі методи для цього зв'язку. Клас Article, метод getCategories():
@ManyToMany(mappedBy = "articles")
public Set<Category> getCategories() {
return categories;
}
І відповідний код в класі Category:
@ManyToMany
@JoinTable(name="category_article")
public Set<Article> getArticles() {
return articles;
}
Грубо кажучи, в одному з класів повинна бути прив'язка до проміжної таблиці category_article, а в другому - прив'язка до першого класу за допомогою виразу mappedBy.
Зверніть увагу, що назви методів getArticles() і getCateories() відповідають назвам полів articles_id і categories_id в таблиці category_article. Тобто, назва поля = назву методу без стартового get.
Додаємо ці класи в конфігураційний файл hibernate.cfg.xml:
<hibernate-configuration>
<session-factory>
...
<mapping class="com.seostella.hibernate.basics.entity.Article"/>
<mapping class="com.seostella.hibernate.basics.entity.Category"/>
....
</session-factory>
</hibernate-configuration>
Розглянемо DAO-класи: ArticleDAO і CategoryDAO. Почнемо з CategoryDAO:
package com.seostella.hibernate.basics.dao;
import com.seostella.hibernate.basics.entity.Category;
import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Query;
/**
*
* @author seostella.com
*/
public class CategoryDAO extends DAO {
public Category createCategory(String title)
throws Exception {
try {
begin();
Category category = new Category(title);
getSession().save(category);
commit();
return category;
} catch (HibernateException e) {
throw new Exception("Could not create category " + title, e);
}
}
public Category retrieveCategory(String categoryTitle) throws Exception {
try {
begin();
Query categoryQuery = getSession().createQuery(
" from Category where title = :categoryTitle");
categoryQuery.setString("categoryTitle", categoryTitle);
Category category = (Category) categoryQuery.uniqueResult();
return category;
} catch (HibernateException e) {
rollback();
throw new Exception("Could not get category " + categoryTitle, e);
}
}
public List<Category> list() throws Exception{
try {
begin();
List<Category> categories = getSession().createQuery("from Category").list();
return categories;
} catch (HibernateException e) {
rollback();
throw new Exception("Could not get category list", e);
}
}
}
Як бачимо, клас CategoryDAO істотно відрізняється від UserDAO лише одним методом list(), який повертає список всіх категорій в базі даних. Аналогічним способом ми могли б отримати список всіх користувачів в базі даних:
List<User> users = getSession().createQuery("from User").list();
Клас ArticleDAO представлено нижче:
package com.seostella.hibernate.basics.dao;
import com.seostella.hibernate.basics.entity.Article;
import com.seostella.hibernate.basics.entity.Category;
import com.seostella.hibernate.basics.entity.User;
import org.hibernate.HibernateException;
/**
*
* @author seostella.com
*/
public class ArticleDAO extends DAO {
public Article createArticle(String username, String categoryTitle,
String title, String message)
throws Exception {
try {
begin();
UserDAO userDAO = new UserDAO();
User user = userDAO.retrieveUser(username);
CategoryDAO categoryDAO = new CategoryDAO();
Category category = categoryDAO.retrieveCategory( categoryTitle );
Article article = new Article(title, message, user);
getSession().save(article);
category.addArticles(article);
getSession().save(category);
commit();
return article;
} catch (HibernateException e) {
throw new Exception("Could not create article " + title, e);
}
}
}
Один існуючий метод createArticle() хоч і здається складним, насправді дуже простий. Спочатку за ім'ям користувача знаходиться об'єкт User, а за ім'ям категорії - об'єкт Category. Використовуючи ці два об'єкти створюється і зберігається в базу даних новий об'єкт Article. Ключовий момент у цьому методі рядки:
category.addArticles(article);
getSession().save(category);
Обов'язково необхідно додати в категорію новий об'єкт Article, інакше зв'язки багато-до-багатьох не буде.
Покажемо на прикладі виведення всіх категорій та пов'язаних з ними статей та користувачів. Створимо клас CategoryApp:
package com.seostella.hibernate.basics;
import com.seostella.hibernate.basics.dao.CategoryDAO;
import com.seostella.hibernate.basics.entity.Article;
import com.seostella.hibernate.basics.entity.Category;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author seostella.com
*/
public class CategoryApp {
private static Logger logger = Logger.getLogger(CategoryApp.class.getName());
public static void main(String[] args) {
try {
List categories = new CategoryDAO().list();
Iterator ci = categories.iterator();
while (ci.hasNext()) {
Category category = (Category) ci.next();
System.out.println("Категория: " + category.getTitle());
Iterator ai = category.getArticles().iterator();
while (ai.hasNext()) {
Article advert = (Article) ai.next();
System.out.println(" Название: " + advert.getTitle());
System.out.println(" Сообщение: " + advert.getMessage());
System.out.println(" Автор: " + advert.getUser().getName());
System.out.println();
}
}
} catch (Exception ex) {
logger.log(Level.SEVERE, null, ex);
}
}
}
При виконанні цієї програми виводиться наступне:
Категория: Биология
Название: "Прикладная" экология
Сообщение: Смысл изучения даннойнауки заключается в получении знания, как сохранить наш дом чистым и пригоднымдля обитания в течение долгих лет. Поскольку целью образования являются не знания, а действия Герберт Спенсер , то анализсуществующего экологического положе
Автор: John Connor
Название: "Законы" экологии Коммонера
Сообщение: К. обращает внимание на всеобщуюсвязь процессов и явлений в природе и близок по смыслу к закону внутреннегодинамического равновесия изменение одного из показателей системы вызываетфункционально-структурные количественные и качественные перемены, при этом
Автор: John Connor
Для демонстрації створення об'єкта Article створимо клас ArticleApp:
package com.seostella.hibernate.basics;
import com.seostella.hibernate.basics.dao.ArticleDAO;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author seostella.com
*/
public class ArticleApp
{
private static Logger logger = Logger.getLogger(ArticleApp.class.getName());
public static void main(String[] args) {
try {
ArticleDAO articleDAO = new ArticleDAO();
articleDAO.createArticle( "John Connor", "Test category", "test title", "test message");
} catch (Exception ex) {
logger.log(Level.SEVERE, null, ex);
}
}
}
Не забудьте додати в базу даних категорію з ім'ям "Test category".
Оригінальний текст програми можна скачати за наступним посиланням - Основи Hibernate
5 січня 2016 р. 11:26
|
Вроде понятный пример для новичка.
Не могу понять куда конкретно добавить в базу данных категорию с именем "Test category". |