Продолжаем цикл статей — переводов по 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 отображен в таблице PERSON. И выглядит следующим образом:
Обратите внимание на аннотацию @OneToMany для переменной creditCards, мы не указали ни каскадный тип, ни fetch-стратегию, полагаясь на настройки по умолчанию. Позже мы узнаем некоторые проблемы, связанные с этим.
Класс CreditCard отображен в таблице CREDIT_CARD и выглядит:
Используя phpmyadmin db designer посмотрим на отношения между Person и CreditCard:
Взглянем на сгенерированные таблицы:
Мы указывали только две сущности: CreditCard и Person, и ожидали увидеть только две таблицы в базе данных. Так почему же их три? Так как в настройке по умолчанию создается третья связующая таблица.
Цитата из Hibernate Annotations Reference Guide:
Без описания отображения, для соотношения один-ко-многим используется связующая таблица. Именем таблицы является конкатенация имени первой таблицы, символа «_» и имени второй таблицы. Для обеспечения соотношения один ко многим колонке с id первой таблицы присваивается модификатор UNIQUE.
Позже мы обсудим другие недостатки установок по умолчанию.
Сервисный слой.
После объявления доменных объектов, нам необходимо создать сервисный слой, который содержит два сервиса: PersonService и CreditCardService.
PersonService отвечает за обработку CRUD операций над сущностью Person. Каждый метод в конечном итоге передает объект Hibernate сессии.
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. Это значит, что мы должны реализовать свою стратегию удаления записей о кредитках.
Вначале необходимо создать запрос для получения кредитных карт, которые мы размещаем на временное хранение в коллекцию. Затем мы удаляем запись о персоне. После всего извлекая из коллекции, мы удаляем кретки одну за другой.
CreditCardService
CreditCardService отвечает за обработку CRUD операций над сущностью CreditCard. Каждый метод в конечном итоге передает объект Hibernate сессии.
Обратим особое внимание на метод delete():
Для удаления кредтки, вначале необходимо удалить ее из связующей таблицы, следующим запросом:
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 страницу.
Обратите внимание, что метод getRecords() метод отображает Person и CreditCard как объект передачи данных: PersonDTO.
PersonDTO выступает в роли модели данных для records.jsp.
CreditCardController
CreditCardController отвечает за обработку запросов для кредиток. Мы не будем использовать все имеющиеся в этом контроллере методы, они были добавлены для полноты.
VIEW слой.
После обсуждения доменного, сервисного и слоя контроллера, создадим слой VIEW. Он состоит в основном из JSP страниц. Вот они:
Добавить новую запись
Правка существующей записи:
Добавить новую кредитку:
Править существующую кредитку:
Конфигурация.
Мы создали все необходимые Java классы. Следующий шаг — создать необходимые файлы конфигурации.
Запуск приложения.
Устанавливаем базу данных.
Наше приложение использует 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
Предыдущая статья:
«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