Spring — Hibernate: ассоциация один ко многим

  • Tutorial
Продолжаем цикл статей — переводов по 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<CreditCard> creditCards;
  
 public Integer getId() {
  return id;
 }
 
 public void setId(Integer id) {
  this.id = id;
 }
 
 public String getFirstName() {
  return firstName;
 }
 
 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }
 
 public String getLastName() {
  return lastName;
 }
 
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 
 public Double getMoney() {
  return money;
 }
 
 public void setMoney(Double money) {
  this.money = money;
 }
 
 public Set<CreditCard> getCreditCards() {
  return creditCards;
 }
 
 public void setCreditCards(Set<CreditCard> creditCards) {
  this.creditCards = creditCards;
 }
}



Класс Person отображен в таблице PERSON. И выглядит следующим образом:



Обратите внимание на аннотацию @OneToMany для переменной creditCards, мы не указали ни каскадный тип, ни fetch-стратегию, полагаясь на настройки по умолчанию. Позже мы узнаем некоторые проблемы, связанные с этим.
Person.java
@Entity
@Table(name = "PERSON")
public class Person implements Serializable {
 
 ...
 
 @OneToMany
 private Set<CreditCard> creditCards;
  
 ...
}


CreditCard.java
package org.krams.tutorial.domain;
 
import java.io.Serializable;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
 
/**
 * Represents a credit card entity
 *
 *  @author Krams at {@link http://krams915@blogspot.com}
 */
@Entity
@Table(name = "CREDIT_CARD")
public class CreditCard implements Serializable {
 
 private static final long serialVersionUID = 5924361831551833717L;
 
 @Id
 @Column(name = "ID")
 @GeneratedValue
 private Integer id;
  
 @Column(name = "TYPE")
 private String type;
  
 @Column(name = "NUMBER")
 private String number;
 
 public Integer getId() {
  return id;
 }
 
 public void setId(Integer id) {
  this.id = id;
 }
 
 public String getType() {
  return type;
 }
 
 public void setType(String type) {
  this.type = type;
 }
 
 public String getNumber() {
  return number;
 }
 
 public void setNumber(String number) {
  this.number = number;
 }
}


Класс CreditCard отображен в таблице CREDIT_CARD и выглядит:



Используя 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<Person> getAll() {
  logger.debug("Retrieving all persons");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
 
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM Person");
   
  // Retrieve all
  return  query.list();
 }
  
 /**
  * Retrieves a single person
  */
 public Person get( Integer id ) {
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // Retrieve existing person
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH  p.creditCards WHERE p.id="+id);
   
  return (Person) query.uniqueResult();
 }
  
 /**
  * Adds a new person
  */
 public void add(Person person) {
  logger.debug("Adding new person");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // Persists to db
  session.save(person);
 }
  
 /**
  * Deletes an existing person
  * @param id the id of the existing person
  */
 public void delete(Integer id) {
  logger.debug("Deleting existing person");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // 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<CreditCard> creditCards =person.getCreditCards();
   
  // Delete person
  session.delete(person);
   
  // Delete associated credit cards
  for (CreditCard creditCard: creditCards) {
   session.delete(creditCard);
  }
 }
  
 /**
  * Edits an existing person
  */
 public void edit(Person person) {
  logger.debug("Editing existing person");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // Retrieve existing person via id
  Person existingPerson = (Person) session.get(Person.class, person.getId());
   
  // Assign updated values to this person
  existingPerson.setFirstName(person.getFirstName());
  existingPerson.setLastName(person.getLastName());
  existingPerson.setMoney(person.getMoney());
 
  // Save updates
  session.save(existingPerson);
 }
}



PersonService обманчиво выглядит простым, он решает две основные проблемы, которые заслуживают особого внимания:

Проблема 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<CreditCard> creditCards =person.getCreditCards();
   
  // Delete person
  session.delete(person);
   
  // Delete associated credit cards
  for (CreditCard creditCard: creditCards) {
   session.delete(creditCard);
  }



CreditCardService
CreditCardService отвечает за обработку CRUD операций над сущностью CreditCard. Каждый метод в конечном итоге передает объект Hibernate сессии.
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<CreditCard> getAll(Integer personId) {
  logger.debug("Retrieving all credit cards");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH  p.creditCards WHERE p.id="+personId);
   
  Person person = (Person) query.uniqueResult();
   
  // Retrieve all
  return  new ArrayList<CreditCard>(person.getCreditCards());
 }
  
 /**
  * Retrieves all credit cards
  */
 public List<CreditCard> getAll() {
  logger.debug("Retrieving all credit cards");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM  CreditCard");
   
  // Retrieve all
  return  query.list();
 }
  
 /**
  * Retrieves a single credit card
  */
 public CreditCard get( Integer id ) {
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // Retrieve existing credit card
  CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);
   
  // Persists to db
  return creditCard;
 }
  
 /**
  * Adds a new credit card
  */
 public void add(Integer personId, CreditCard creditCard) {
  logger.debug("Adding new credit card");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Persists to db
  session.save(creditCard);
   
  // Add to person as well
  // Retrieve existing person via id
  Person existingPerson = (Person) session.get(Person.class, personId);
   
  // Assign updated values to this person
  existingPerson.getCreditCards().add(creditCard);
 
  // Save updates
  session.save(existingPerson);
 }
  
 /**
  * Deletes an existing credit card
  */
 public void delete(Integer id) {
  logger.debug("Deleting existing credit card");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
     // Delete reference to foreign key credit card first
  // We need a SQL query instead of HQL query here to access the third table
     Query query = session.createSQLQuery("DELETE FROM PERSON_CREDIT_CARD " +
       "WHERE creditCards_ID="+id);
      
     query.executeUpdate();
      
  // Retrieve existing credit card
  CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);
   
  // Delete
  session.delete(creditCard);
 }
  
 /**
  * Edits an existing credit card
  */
 public void edit(CreditCard creditCard) {
  logger.debug("Editing existing creditCard");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
  // Retrieve existing credit card via id
  CreditCard existingCreditCard = (CreditCard) session.get(CreditCard.class, creditCard.getId());
   
  // Assign updated values to this credit card
  existingCreditCard.setNumber(creditCard.getNumber());
  existingCreditCard.setType(creditCard.getType());
 
  // Save updates
  session.save(existingCreditCard);
 }
}


Обратим особое внимание на метод delete():
delete()
public void delete(Integer id) {
  logger.debug("Deleting existing credit card");
   
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
   
     // Delete reference to foreign key credit card first
  // We need a SQL query instead of HQL query here to access the third table
     Query query = session.createSQLQuery("DELETE FROM PERSON_CREDIT_CARD " +
       "WHERE creditCards_ID="+id);
      
     query.executeUpdate();
      
  // Retrieve existing credit card
  CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);
   
  // Delete
  session.delete(creditCard);
 }



Для удаления кредтки, вначале необходимо удалить ее из связующей таблицы, следующим запросом:
Query query = session.createSQLQuery(«DELETE FROM PERSON_CREDIT_CARD » +
«WHERE creditCards_ID=»+id);


Обратите внимание, что связующая таблица создана 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<Person> persons = personService.getAll();
      
     // Prepare model object
     List<PersonDTO> personsDTO = new ArrayList<PersonDTO>();
      
     for (Person person: persons) {
      // Create new data transfer object
      PersonDTO dto = new PersonDTO();
       
   dto.setId(person.getId());
   dto.setFirstName(person.getFirstName());
   dto.setLastName(person.getLastName());
   dto.setMoney(person.getMoney());
   dto.setCreditCards(creditCardService.getAll(person.getId()));
    
   // Add to model list
   personsDTO.add(dto);
     }
      
     // Add to model
     model.addAttribute("persons", personsDTO);
 
     // This will resolve to /WEB-INF/jsp/records.jsp
  return "records";
 }
     
    /**
     *  Retrieves the "Add New Record" page
     */
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
     
     // Create new Person and add to model
     model.addAttribute("personAttribute", new Person());
 
     // This will resolve to /WEB-INF/jsp/add-record.jsp
     return "add-record";
 }
  
    /**
     * Adds a new record
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String postAdd(@ModelAttribute("personAttribute") Person person) {
  logger.debug("Received request to add new record");
 
  // Delegate to service
  personService.add(person);
 
  // Redirect to url
  return "redirect:/krams/main/record/list";
 }
     
    /**
     * Deletes a record including all the associated credit cards
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDelete(@RequestParam("id") Integer personId) {
     logger.debug("Received request to delete record");
      
     // Delete person
  personService.delete(personId);
 
  // Redirect to url
  return "redirect:/krams/main/record/list";
 }
     
    /**
     * Retrieves the "Edit Existing Record" page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam("id") Integer personId, Model model) {
     logger.debug("Received request to show edit page");
      
     // Retrieve person by id
     Person existingPerson = personService.get(personId);
 
     // Add to model
     model.addAttribute("personAttribute", existingPerson);
 
     // This will resolve to /WEB-INF/jsp/edit-record.jsp
     return "edit-record";
 }
  
    /**
     * Edits an existing record
     */
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String postEdit(@RequestParam("id") Integer personId,
              @ModelAttribute("personAttribute") Person person) {
  logger.debug("Received request to edit existing person");
   
  // Assign id
  person.setId(personId);
   
  // Delegate to service
  personService.edit(person);
 
  // Redirect to url
  return "redirect:/krams/main/record/list";
 }
     
}



Обратите внимание, что метод getRecords() метод отображает Person и CreditCard как объект передачи данных: PersonDTO.
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<Person> persons = personService.getAll();
      
     // Prepare model object
     List<PersonDTO> personsDTO = new ArrayList<PersonDTO>();
      
     for (Person person: persons) {
      // Create new data transfer object
      PersonDTO dto = new PersonDTO();
       
   dto.setId(person.getId());
   dto.setFirstName(person.getFirstName());
   dto.setLastName(person.getLastName());
   dto.setMoney(person.getMoney());
   dto.setCreditCards(creditCardService.getAll(person.getId()));
    
   // Add to model list
   personsDTO.add(dto);
     }
      
     // Add to model
     model.addAttribute("persons", personsDTO);
 
     // This will resolve to /WEB-INF/jsp/records.jsp
  return "records";
 }



PersonDTO выступает в роли модели данных для records.jsp.
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<CreditCard> creditCards;
  
 public Integer getId() {
  return id;
 }
 public void setId(Integer id) {
  this.id = id;
 }
 public String getFirstName() {
  return firstName;
 }
 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }
 public String getLastName() {
  return lastName;
 }
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 public Double getMoney() {
  return money;
 }
 public void setMoney(Double money) {
  this.money = money;
 }
 public List<CreditCard> getCreditCards() {
  return creditCards;
 }
 public void setCreditCards(List<CreditCard> creditCards) {
  this.creditCards = creditCards;
 }
}



CreditCardController
CreditCardController отвечает за обработку запросов для кредиток. Мы не будем использовать все имеющиеся в этом контроллере методы, они были добавлены для полноты.

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"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Records</h1>
<c:url var="editImgUrl" value="/resources/img/edit.png" />
<c:url var="deleteImgUrl" value="/resources/img/delete.png" />
<c:url var="addUrl" value="/krams/main/record/add" />
<table style="border: 1px solid; width: 100%; text-align:center">
 <thead style="background:#d3dce3">
  <tr>
   <th>Id</th>
   <th>First Name</th>
   <th>Last Name</th>
   <th>Money</th>
   <th colspan="2"></th>
   <th>CC Type</th>
   <th>CC Number</th>
   <th colspan="3"></th>
  </tr>
 </thead>
 <tbody style="background:#ccc">
 <c:forEach items="${persons}" var="person">
  <c:url var="editUrl" value="/krams/main/record/edit?id=${person.id}" />
  <c:url var="deleteUrl" value="/krams/main/record/delete?id=${person.id}" />
   
  <c:if test="${!empty person.creditCards}">
   <c:forEach items="${person.creditCards}" var="creditCard">
   <tr>
    <td><c:out value="${person.id}" /></td>
    <td><c:out value="${person.firstName}" /></td>
    <td><c:out value="${person.lastName}" /></td>
    <td><c:out value="${person.money}" /></td>
    <td><a href="${editUrl}"><img src="${editImgUrl}"></img></a></td>
    <td><a href="${deleteUrl}"><img src="${deleteImgUrl}"></img></a></td>
     
    <td><c:out value="${creditCard.type}" /></td>
    <td><c:out value="${creditCard.number}" /></td>
    <c:url var="addCcUrl" value="/krams/main/creditcard/add?id=${person.id}" />
    <c:url var="editCcUrl" value="/krams/main/creditcard/edit?pid=${person.id}&cid=${creditCard.id}" />
    <c:url var="deleteCcUrl" value="/krams/main/creditcard/delete?id=${creditCard.id}" />
    <td><a href="${addCcUrl}">+</a></td>
    <td><a href="${editCcUrl}"><img src="${editImgUrl}"></img></a></td>
    <td><a href="${deleteCcUrl}"><img src="${deleteImgUrl}"></img></a></td>
   </tr>
   </c:forEach>
  </c:if>
   
  <c:if test="${empty person.creditCards}">
   <tr>
    <td><c:out value="${person.id}" /></td>
    <td><c:out value="${person.firstName}" /></td>
    <td><c:out value="${person.lastName}" /></td>
    <td><c:out value="${person.money}" /></td>
    <td><a href="${editUrl}"><img src="${editImgUrl}"></img></a></td>
    <td><a href="${deleteUrl}"><img src="${deleteImgUrl}"></img></a></td>
     
    <td>N/A</td>
    <td>N/A</td>
    <c:url var="addCcUrl" value="/krams/main/creditcard/add?id=${person.id}" />
    <td><a href="${addCcUrl}">+</a></td>
    <td></td>
    <td></td>
   </tr>
  </c:if>
   
 </c:forEach>
 </tbody>
</table>
 
<c:if test="${empty persons}">
 No records found.
</c:if>
 
<p><a href="${addUrl}">Create new record</a></p>
 
</body>
</html>



Добавить новую запись

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"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h1>Create New Record</h1>
<c:url var="saveUrl" value="/krams/main/record/add" />
<form:form modelAttribute="personAttribute" method="POST" action="${saveUrl}">
 <table>
  <tr>
   <td><form:label path="firstName">First Name:</form:label></td>
   <td><form:input path="firstName"/></td>
  </tr>
 
  <tr>
   <td><form:label path="lastName">Last Name</form:label></td>
   <td><form:input path="lastName"/></td>
  </tr>
   
  <tr>
   <td><form:label path="money">Money</form:label></td>
   <td><form:input path="money"/></td>
  </tr>
 </table>
  
 <input type="submit" value="Save" />
</form:form>
 
</body>
</html>



Правка существующей записи:

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"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h1>Edit Existing Record</h1>
<c:url var="saveUrl" value="/krams/main/record/edit?id=${personAttribute.id}" />
<form:form modelAttribute="personAttribute" method="POST" action="${saveUrl}">
 <table>
  <tr>
   <td><form:label path="id">Id:</form:label></td>
   <td><form:input path="id" disabled="true"/></td>
  </tr>
  
  <tr>
   <td><form:label path="firstName">First Name:</form:label></td>
   <td><form:input path="firstName"/></td>
  </tr>
 
  <tr>
   <td><form:label path="lastName">Last Name</form:label></td>
   <td><form:input path="lastName"/></td>
  </tr>
   
  <tr>
   <td><form:label path="money">Money</form:label></td>
   <td><form:input path="money"/></td>
  </tr>
 </table>
  
 <input type="submit" value="Save" />
</form:form>
 
</body>
</html>



Добавить новую кредитку:

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"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h1>Add New Credit Card</h1>
<c:url var="saveUrl" value="/krams/main/creditcard/add?id=${personId}" />
<form:form modelAttribute="creditCardAttribute" method="POST" action="${saveUrl}">
 <table>
  
  <tr>
   <td>Person Id:</td>
   <td><input type="text" value="${personId}" disabled="true"/>
  </tr>
   
  <tr>
   <td><form:label path="type">Type:</form:label></td>
   <td><form:input path="type"/></td>
  </tr>
 
  <tr>
   <td><form:label path="number">Number:</form:label></td>
   <td><form:input path="number"/></td>
  </tr>
 </table>
  
 <input type="submit" value="Save" />
</form:form>
 
</body>
</html>



Править существующую кредитку:

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"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h1>Edit Existing Credit Card</h1>
<c:url var="saveUrl" value="/krams/main/creditcard/edit?id=${creditCardAttribute.id}" />
<form:form modelAttribute="creditCardAttribute" method="POST" action="${saveUrl}">
 <table>
  
  <tr>
   <td>Person Id:</td>
   <td><input type="text" value="${personId}" disabled="true"/>
  </tr>
   
  <tr>
   <td><form:label path="type">Type:</form:label></td>
   <td><form:input path="type"/></td>
  </tr>
 
  <tr>
   <td><form:label path="number">Number:</form:label></td>
   <td><form:input path="number"/></td>
  </tr>
 </table>
  
 <input type="submit" value="Save" />
</form:form>
 
</body>
</html>



Конфигурация.
Мы создали все необходимые Java классы. Следующий шаг — создать необходимые файлы конфигурации.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
  
 <servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>
  
 <servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/krams/*</url-pattern>
 </servlet-mapping>
 
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
  
</web-app>



spring-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:p="http://www.springframework.org/schema/p"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
  
 <!-- Declare a view resolver -->
 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
      p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
 
</beans>



applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:mvc="http://www.springframework.org/schema/mvc"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd
   http://www.springframework.org/schema/mvc
   http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
  
 <!-- Activates various annotations to be detected in bean classes -->
 <context:annotation-config />
  
 <!-- Scans the classpath for annotated components that will be auto-registered as Spring beans.
  For example @Controller and @Service. Make sure to set the correct base-package-->
 <context:component-scan base-package="org.krams.tutorial" />
  
 <!-- Configures the annotation-driven Spring MVC Controller programming model.
 Note that, with Spring 3.0, this tag works in Servlet MVC only!  -->
 <mvc:annotation-driven />
  
 <!-- Load Hibernate related configuration -->
 <import resource="hibernate-context.xml" />
  
</beans>



hibernate-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans  xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/tx
   http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context-3.0.xsd
      ">
 
 <context:property-placeholder location="/WEB-INF/spring.properties" />
     
    <!-- Enable annotation style of managing transactions -->
 <tx:annotation-driven transaction-manager="transactionManager" />
    
    <!-- Declare the Hibernate SessionFactory for retrieving Hibernate sessions -->
    <!-- See http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/orm/hibernate3/annotation/AnnotationSessionFactoryBean.html -->       
 <!-- See http://docs.jboss.org/hibernate/stable/core/api/index.html?org/hibernate/SessionFactory.html -->
 <!-- See http://docs.jboss.org/hibernate/stable/core/api/index.html?org/hibernate/Session.html -->
 <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
     p:dataSource-ref="dataSource"
     p:configLocation="${hibernate.config}"
     p:packagesToScan="org.krams.tutorial"/>
  
 <!-- Declare a datasource that has pooling capabilities--> 
 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
    destroy-method="close"
    p:driverClass="${app.jdbc.driverClassName}"
    p:jdbcUrl="${app.jdbc.url}"
    p:user="${app.jdbc.username}"
    p:password="${app.jdbc.password}"
    p:acquireIncrement="5"
    p:idleConnectionTestPeriod="60"
    p:maxPoolSize="100"
    p:maxStatements="50"
    p:minPoolSize="10" />
 
 <!-- Declare a transaction manager-->
 <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory" />
   
</beans>



hibernate.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
   
<hibernate-configuration>
  <session-factory>
   <!-- We're using MySQL database so the dialect needs to MySQL as well-->
    <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
    <!-- Enable this to see the SQL statements in the logs-->
    <property name="show_sql">true</property>
    <!-- This will drop our existing database and re-create a new one.
      Existing data will be deleted! -->
    <property name="hbm2ddl.auto">create</property>
  </session-factory>
</hibernate-configuration>



spring.properties
# database properties
app.jdbc.driverClassName=com.mysql.jdbc.Driver
app.jdbc.url=jdbc:mysql://localhost/mydatabase
app.jdbc.username=root
app.jdbc.password=
 
#hibernate properties
hibernate.config=/WEB-INF/hibernate.cfg.xml



Запуск приложения.
Устанавливаем базу данных.
Наше приложение использует MySQL в качестве базы данных. Для запуска приложения убедитесь, что вы создали базу данных.

Для ее создания следуйте шагам:
1. Откройте phpmyadmin (или любую другую программу для работы с БД, которую вы предпочитаете)
2. Создаем новую базу данных mydatabase
3. Запускаем наше приложение, схему БД оно создаст автоматически.

Для проверки используете файл 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
  • +6
  • 37.4k
  • 8
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 8

    +2
    4-ый Spring уже давно зарелизен ;)
      0
      Вы сказали, что есть проблемы, но не описали их решения, только воркэраунды, для новичка будет трудно разобраться отличие каскадных типов.
        0
        Это будет обсуждено через пару статей, когда будут использоваться эти стратегии
        0
        Не кажется мне что это хороший тьюториал. Право же, зачем учить как не надо делать? Если сейчас нет желания объяснять все настройки, лучше просто написать «делать так». И DTO здесь не к месту, есть же OpenSessionInViewInterceptor.
          0
          ну, вы ещё начните придираться к тому, что в Service идёт прямая работа с БД и что не используется Spring Data для CRUD.
        0
        ну ведь старье!) jsp уже можно в гроб класть. работу через родную хибер-сессию тоже. Давно уже есть JPA, недавно появился Spring Data.
          0
          1) Попробуйте покрыть свой код юнит-тестами на 100% (сможете увидеть несколько неудачных архитектурных решений).
          2) Посмотрите как это же приложение можно было сделать на Grails (там под капотом те же спринг и гибернейт, но кода всего строчек 100 будет, считая импорты)

          Only users with full accounts can post comments. Log in, please.