Spring — Hibernate: ассоциация один ко многим
Продолжаем цикл статей — переводов по Spring и Hibernate, от krams.Предыдущая статья: «Spring MVC 3, Аннотации Hibernate, MySQL. Туториал по интеграции».
Введение.В этом уроке мы познакомимся с использованием отношения один ко многим, используя аннотации Hibernate и Spring MVC 3. Мы будем использовать аннотоцию @OneToMany для указания отношений между нашими объектами. Мы не будем использовать каскадные типы или fetch-стратегии, вместо этого мы воспользуемся стандартными настройками @OneToMany.
Что такое ассоциация один-ко-многим? Ассоциация один-ко-многим возникает тогда, когда каждой записи в таблице А, соответствует множество записей в таблице Б, но каждая запись в таблице Б имеет лишь одну соответствующую запись в таблице А.
Спецификация нашего приложения.
Приложение является простой CRUD системой управления списком записей. Каждая запись соответствует одному лицу, она содержит персональные данные и данные о кредитной карте. Каждое лицо может владеть несколькими кредитками. Так же мы добавим систему редактирования лиц и кредиток.
Ниже приведены скриншоты из будущего приложения:
Доменные объекты
Основываясь на спецификации, мы имеем два доменных объекта: персона (Person) и кредитка (Credit Card).
У объекта персоны должны быть следующие поля: — id— first name (имя)— last name (фамилия)— money (деньги)— credit cards (кредитки)
У объекта кредитка поля следующие: — id— type (тип)— number (номер)
Обратите внимание, что каждой персоне соответствует множество кредитных карт, а следственно мы используем ассоциацию один-ко-многим. Конечно мы можем посмотреть на эту ситуацию и с другой стороны и использовать ассоциацию многие-к-одному, но это будет темой другого урока.
РазработкаМы разобьем нашу разработку на три слоя: доменный, сервисный и контроллер, затем укажем конфигурационные файлы.
Начнем с доменного слоя.Как говорилось ранее, у нас есть два доменных объекта: Person и CreditCard. Следовательно мы объявим два POJO объекта, представляющих наш доменный слой. У каждого из них будет аннотация Entity для хранения в базе данных.
Person.java
package org.krams.tutorial.domain;
import java.io.Serializable;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
/**
* Represents a person entity
*
* @author Krams at {@link http://krams915@blogspot.com}
*/
@Entity
@Table (name = «PERSON»)
public class Person implements Serializable {
private static final long serialVersionUID = -5527566248002296042L;
@Id
@Column (name = «ID»)
@GeneratedValue
private Integer id;
@Column (name = «FIRST_NAME»)
private String firstName;
@Column (name = «LAST_NAME»)
private String lastName;
@Column (name = «MONEY»)
private Double money;
@OneToMany
private Set
Обратите внимание на аннотацию @OneToMany для переменной creditCards, мы не указали ни каскадный тип, ни fetch-стратегию, полагаясь на настройки по умолчанию. Позже мы узнаем некоторые проблемы, связанные с этим.
Person.java
@Entity
@Table (name = «PERSON»)
public class Person implements Serializable {
…
@OneToMany
private Set
Используя phpmyadmin db designer посмотрим на отношения между Person и CreditCard:
Взглянем на сгенерированные таблицы:
Мы указывали только две сущности: CreditCard и Person, и ожидали увидеть только две таблицы в базе данных. Так почему же их три? Так как в настройке по умолчанию создается третья связующая таблица.
Цитата из Hibernate Annotations Reference Guide: Без описания отображения, для соотношения один-ко-многим используется связующая таблица. Именем таблицы является конкатенация имени первой таблицы, символа »_» и имени второй таблицы. Для обеспечения соотношения один ко многим колонке с id первой таблицы присваивается модификатор UNIQUE.Позже мы обсудим другие недостатки установок по умолчанию.
Сервисный слой.После объявления доменных объектов, нам необходимо создать сервисный слой, который содержит два сервиса: PersonService и CreditCardService.
PersonService отвечает за обработку CRUD операций над сущностью Person. Каждый метод в конечном итоге передает объект Hibernate сессии.
PersonService.java
package org.krams.tutorial.service;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Service for processing Persons
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Service («personService»)
@Transactional
public class PersonService {
protected static Logger logger = Logger.getLogger («service»);
@Resource (name=«sessionFactory»)
private SessionFactory sessionFactory;
/**
* Retrieves all persons
*
* @return a list of persons
*/
public List
Проблема 1. Fetch стратегииПолучение записи лица не подгружает связанные с ним записи кредиток.
Следующий запрос получает персону по id.Query query = session.createQuery («FROM Person WHERE p.id=»+id);
Проблема происходит потому, что мы не указали fetch-стратегию когда описывали аннотацию @OneToMany, для того, что бы это исправить — изменим запрос: Query query = session.createQuery («FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id=»+id);
Проблема 2. Каскадные типы.Удаление персоны не приводит к удалению соответствуещих ему кредитных карт.Следующая запись удаляет запись о лице: session.delete (person);
Проблема возникает потому, что мы не указали каскадные типы в аннотации @OneToMany. Это значит, что мы должны реализовать свою стратегию удаления записей о кредитках.
Вначале необходимо создать запрос для получения кредитных карт, которые мы размещаем на временное хранение в коллекцию. Затем мы удаляем запись о персоне. После всего извлекая из коллекции, мы удаляем кретки одну за другой.
Исправленный запрос
// Create a Hibernate query (HQL)
Query query = session.createQuery («FROM Person as p LEFT JOIN FETCH p.creditCards WHERE p.id=»+id);
// Retrieve record
Person person = (Person) query.uniqueResult ();
Set
CreditCardService.java
package org.krams.tutorial.service;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Service for processing Credit Cards
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Service («creditCardService»)
@Transactional
public class CreditCardService {
protected static Logger logger = Logger.getLogger («service»);
@Resource (name=«sessionFactory»)
private SessionFactory sessionFactory;
/**
* Retrieves all credit cards
*/
public List
Обратите внимание, что связующая таблица создана Hibernate и запрос SQL, а не HQL.После удаления данных из связующей таблицы, удаляем информацию из таблицы CREDIT_CARD.session.delete (creditCard)
Слой контроллера.После создания сервисного и доменного слоев, необходимо создать слой контроллер. Мы создадим два контроллера: MainController и CreditCardController.
MainController
MainController отвечает за обработку запросов к записям лиц. Каждый CRUD запрос в конечном счете передается на PersonService, а затем возвращает соответствующую JSP страницу.
MainController.java
package org.krams.tutorial.controller;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.krams.tutorial.dto.PersonDTO;
import org.krams.tutorial.service.CreditCardService;
import org.krams.tutorial.service.PersonService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
* Handles person request
*
* @author Krams at {@link http://krams915@blogspot.com
*/
@Controller
@RequestMapping (»/main/record»)
public class MainController {
protected static Logger logger = Logger.getLogger («controller»);
@Resource (name=«personService»)
private PersonService personService;
@Resource (name=«creditCardService»)
private CreditCardService creditCardService;
/**
* Retrieves the «Records» page
*/
@RequestMapping (value = »/list», method = RequestMethod.GET)
public String getRecords (Model model) {
logger.debug («Received request to show records page»);
// Retrieve all persons
List
getRecords ()
/**
* Retrieves the «Records» page
*/
@RequestMapping (value = »/list», method = RequestMethod.GET)
public String getRecords (Model model) {
logger.debug («Received request to show records page»);
// Retrieve all persons
List
PersonDTO.java
package org.krams.tutorial.dto;
import java.util.List;
import org.krams.tutorial.domain.CreditCard;
/**
* Data Transfer Object for displaying purposes
*/
public class PersonDTO {
private Integer id;
private String firstName;
private String lastName;
private Double money;
private List
CreditCardController.java package org.krams.tutorial.controller; import javax.annotation.Resource; import org.apache.log4j.Logger; import org.krams.tutorial.domain.CreditCard; import org.krams.tutorial.service.CreditCardService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; /** * Handles credit card requests * * @author Krams at {@link http://krams915@blogspot.com */ @Controller @RequestMapping (»/main/creditcard») public class CreditCardController { protected static Logger logger = Logger.getLogger («controller»); @Resource (name=«creditCardService») private CreditCardService creditCardService; /** * Retrieves the «Add New Credit Card» page */ @RequestMapping (value = »/add», method = RequestMethod.GET) public String getAdd (@RequestParam («id») Integer personId, Model model) { logger.debug («Received request to show add page»); // Prepare model object CreditCard creditCard = new CreditCard (); // Add to model model.addAttribute («personId», personId); model.addAttribute («creditCardAttribute», creditCard); // This will resolve to /WEB-INF/jsp/add-credit-card.jsp return «add-credit-card»; } /** * Adds a new credit card */ @RequestMapping (value = »/add», method = RequestMethod.POST) public String postAdd (@RequestParam («id») Integer personId, @ModelAttribute («creditCardAttribute») CreditCard creditCard) { logger.debug («Received request to add new credit card»); // Delegate to service creditCardService.add (personId, creditCard); // Redirect to url return «redirect:/krams/main/record/list»; } /** * Deletes a credit card */ @RequestMapping (value = »/delete», method = RequestMethod.GET) public String getDelete (@RequestParam («id») Integer creditCardId) { logger.debug («Received request to delete credit card»); // Delegate to service creditCardService.delete (creditCardId); // Redirect to url return «redirect:/krams/main/record/list»; } /** * Retrieves the «Edit Existing Credit Card» page */ @RequestMapping (value = »/edit», method = RequestMethod.GET) public String getEdit (@RequestParam («pid») Integer personId, @RequestParam («cid») Integer creditCardId, Model model) { logger.debug («Received request to show edit page»); // Retrieve credit card by id CreditCard existingCreditCard = creditCardService.get (creditCardId); // Add to model model.addAttribute («personId», personId); model.addAttribute («creditCardAttribute», existingCreditCard); // This will resolve to /WEB-INF/jsp/edit-credit-card.jsp return «edit-credit-card»; } /** * Edits an existing credit card */ @RequestMapping (value = »/edit», method = RequestMethod.POST) public String postEdit (@RequestParam («id») Integer creditCardId, @ModelAttribute («creditCardAttribute») CreditCard creditCard) { logger.debug («Received request to add new credit card»); // Assign id creditCard.setId (creditCardId); // Delegate to service creditCardService.edit (creditCard); // Redirect to url return «redirect:/krams/main/record/list»; } } VIEW слой.
После обсуждения доменного, сервисного и слоя контроллера, создадим слой VIEW. Он состоит в основном из JSP страниц. Вот они:
records.jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
Records
Id | First Name | Last Name | Money | CC Type | CC Number | |||||
---|---|---|---|---|---|---|---|---|---|---|
+ | ||||||||||
N/A | N/A | + |
add-record.jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
Create New Record
edit-record.jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
Edit Existing Record
add-credit-card.jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
Add New Credit Card
Person Id: | |
edit-credit-card.jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
Edit Existing Credit Card
Person Id: | |
web.xml
Для ее создания следуйте шагам:1. Откройте phpmyadmin (или любую другую программу для работы с БД, которую вы предпочитаете)2. Создаем новую базу данных mydatabase3. Запускаем наше приложение, схему БД оно создаст автоматически.
Для проверки используете файл mydatabase.sql, расположенный в каталоге WEB_INF нашего приложения.
Для доступа к приложению используйте URL: localhost:8080/spring-hibernate-one-to-many-default/krams/main/record/list
ЗаключениеМы создали приложения Spring MVC используя отношение один-ко-многим и аннотации Hibernate. Так же мы обсудили проблемы связанные с установками по умолчанию для аннотации @OneToMany.
GIT: github.com/sa4ek/spring-hibernate-one-to-many-default