JPA — пример приложения Hello World. Настройка использования Spring + JPA + Hibernate. Часть 2
Это вторая часть статьи JPA — пример приложения Hello World. Настройка использования Spring + JPA + Hibernate.
7. Листинг созданных сущностей
IntelliJ IDEA любезно помогла создать нам сущности в Java классах для каждой из таблиц схемы javastudy:
Если вы до этого не работали с БД и Hibernate (или аналогами), то вас может запутать созданный код. Но на самом деле там всего лишь несколько геттеров\сеттеров для каждой колонки из таблицы, переопределение методов hashCode(), toString() и equals().
Вам же следует обратить внимание только на аннотации @OneToMany (@ManyToOne) и @ManyToMany.
Основная таблица contact:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
package ru.javastudy.entities; import javax.persistence.*; import java.sql.Date; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "contact", schema = "", catalog = "javastudy") @NamedQueries({ @NamedQuery(name = "ContactEntity.findAll", query = "select c from ContactEntity c"), @NamedQuery(name = "ContactEntity.findById", query = "select distinct c from ContactEntity c left join fetch c.contactTelDetails t left join fetch c.hobbies h where c.id = :id"), @NamedQuery(name="ContactEntity.findAllWithDetail", query="select distinct c from ContactEntity c left join fetch c.contactTelDetails t left join fetch c.hobbies h") }) public class ContactEntity { private int id; private String firstName; private String lastName; private Date birthDate; private int version; @Id @Column(name = "id", nullable = false, insertable = true, updatable = true) public int getId() { return id; } public void setId(int id) { this.id = id; } @Basic @Column(name = "first_name", nullable = false, insertable = true, updatable = true, length = 60) public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @Basic @Column(name = "last_name", nullable = false, insertable = true, updatable = true, length = 40) public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Basic @Column(name = "birth_date", nullable = true, insertable = true, updatable = true) public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } @Basic @Column(name = "version", nullable = false, insertable = true, updatable = true) public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } private Set<ContactTelDetailEntity> contactTelDetails = new HashSet<ContactTelDetailEntity>(); @OneToMany(mappedBy = "contact", cascade = CascadeType.ALL, orphanRemoval = true) public Set<ContactTelDetailEntity> getContactTelDetails() { return contactTelDetails; } public void setContactTelDetails(Set<ContactTelDetailEntity> contactTelDetails) { this.contactTelDetails = contactTelDetails; } private Set<HobbyEntity> hobbies = new HashSet<HobbyEntity>(); @ManyToMany @JoinTable(name = "contact_hobby_detail", //foreign key for ContactEntity in contact_hobby_detail table joinColumns = @JoinColumn(name = "contact_id"), //key for other side - hobby table inverseJoinColumns = @JoinColumn(name = "hobby_id")) public Set<HobbyEntity> getHobbies() { return hobbies; } public void setHobbies(Set<HobbyEntity> hobbies) { this.hobbies = hobbies; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ContactEntity that = (ContactEntity) o; if (id != that.id) return false; if (version != that.version) return false; if (firstName != null ? !firstName.equals(that.firstName) : that.firstName != null) return false; if (lastName != null ? !lastName.equals(that.lastName) : that.lastName != null) return false; if (birthDate != null ? !birthDate.equals(that.birthDate) : that.birthDate != null) return false; return true; } @Override public int hashCode() { int result = id; result = 31 * result + (firstName != null ? firstName.hashCode() : 0); result = 31 * result + (lastName != null ? lastName.hashCode() : 0); result = 31 * result + (birthDate != null ? birthDate.hashCode() : 0); result = 31 * result + version; return result; } @Override public String toString() { return "ContactEntity{" + "id=" + id + ", " + firstName + '\'' + ", " + lastName + '\'' + ", " + birthDate + ", " + version + '}'; } } |
contact_hobby_detail:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
package ru.javastudy.entities; import javax.persistence.*; @Entity @Table(name = "contact_hobby_detail", schema = "", catalog = "javastudy") @IdClass(ContactHobbyDetailEntityPK.class) public class ContactHobbyDetailEntity { private int contactId; private String hobbyId; private ContactEntity contactByContactId; @Id @Column(name = "contact_id", nullable = false, insertable = true, updatable = true) public int getContactId() { return contactId; } public void setContactId(int contactId) { this.contactId = contactId; } @Id @Column(name = "hobby_id", nullable = false, insertable = true, updatable = true, length = 20) public String getHobbyId() { return hobbyId; } public void setHobbyId(String hobbyId) { this.hobbyId = hobbyId; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ContactHobbyDetailEntity that = (ContactHobbyDetailEntity) o; if (contactId != that.contactId) return false; if (hobbyId != null ? !hobbyId.equals(that.hobbyId) : that.hobbyId != null) return false; return true; } @Override public int hashCode() { int result = contactId; result = 31 * result + (hobbyId != null ? hobbyId.hashCode() : 0); return result; } @ManyToOne @JoinColumn(name = "contact_id", referencedColumnName = "id", nullable = false) public ContactEntity getContactByContactId() { return contactByContactId; } public void setContactByContactId(ContactEntity contactByContactId) { this.contactByContactId = contactByContactId; } } |
contact_tel_detail:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
package ru.javastudy.entities; import javax.persistence.*; @Entity @Table(name = "contact_tel_detail", schema = "", catalog = "javastudy") public class ContactTelDetailEntity { private int id; private String telType; private String telNumber; private int version; private ContactEntity contactByContactId; @Id @Column(name = "id", nullable = false, insertable = true, updatable = true) public int getId() { return id; } public void setId(int id) { this.id = id; } @Basic @Column(name = "tel_type", nullable = false, insertable = true, updatable = true, length = 20) public String getTelType() { return telType; } public void setTelType(String telType) { this.telType = telType; } @Basic @Column(name = "tel_number", nullable = false, insertable = true, updatable = true, length = 20) public String getTelNumber() { return telNumber; } public void setTelNumber(String telNumber) { this.telNumber = telNumber; } @Basic @Column(name = "version", nullable = false, insertable = true, updatable = true) public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } private ContactEntity contact; @ManyToOne @JoinColumn(name = "contact_id") public ContactEntity getContact() { return contact; } public void setContact(ContactEntity contact) { this.contact = contact; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ContactTelDetailEntity that = (ContactTelDetailEntity) o; if (id != that.id) return false; if (version != that.version) return false; if (telType != null ? !telType.equals(that.telType) : that.telType != null) return false; if (telNumber != null ? !telNumber.equals(that.telNumber) : that.telNumber != null) return false; return true; } @Override public int hashCode() { int result = id; result = 31 * result + (telType != null ? telType.hashCode() : 0); result = 31 * result + (telNumber != null ? telNumber.hashCode() : 0); result = 31 * result + version; return result; } @Override public String toString() { return "ContactTelDetailEntity{" + "version=" + version + ", telNumber='" + telNumber + '\'' + ", telType='" + telType + '\'' + ", id=" + id + '}'; } } |
и последняя hobby:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package ru.javastudy.entities; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "hobby", schema = "", catalog = "javastudy") public class HobbyEntity { private String hobbyId; @Id @Column(name = "hobby_id", nullable = false, insertable = true, updatable = true, length = 20) public String getHobbyId() { return hobbyId; } public void setHobbyId(String hobbyId) { this.hobbyId = hobbyId; } private Set<ContactEntity> contacts = new HashSet<ContactEntity>(); @ManyToMany @JoinTable(name = "contact_hobby_detail", //foreign key for HobbyEntity in contact_hobby_detail table joinColumns = @JoinColumn(name = "hobby_id"), //key for other side - contact table inverseJoinColumns = @JoinColumn(name = "contact_id")) public Set<ContactEntity> getContacts() { return contacts; } public void setContacts(Set<ContactEntity> contacts) { this.contacts = contacts; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; HobbyEntity that = (HobbyEntity) o; if (hobbyId != null ? !hobbyId.equals(that.hobbyId) : that.hobbyId != null) return false; return true; } @Override public int hashCode() { return hobbyId != null ? hobbyId.hashCode() : 0; } @Override public String toString() { return "HobbyEntity{" + "hobbyId='" + hobbyId + '\'' + '}'; } } |
Не буду вдаваться в подробное описание кода в каждой из сущностей, т.к. оно есть в цикле из Hibernate. В общих словах:
- таблица contact (сущность ContactEntity) имеет отношение один-ко-многим с таблицей contact_tel_detail. Т.е. у одного контакта может быть несколько телефонов.
А так же отношение многие-ко-многим с таблицей hobby. Т.е. у одного контакта может ноль и более хобби и одно хобби может быть у нуля или более контактов. - над сущностью ContactEntity созданы именованные запросы. Имена говорят сами за себя.
8. Интерфейс и реализация для доступа к данным
Был создан простой интерфейс ContactService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package ru.javastudy.intf; import ru.javastudy.entities.ContactEntity; import java.util.List; public interface ContactService { // Найти все контакты. public List<ContactEntity> findAll(); // Найти все контакты с заданным телефоном и хобби. public List<ContactEntity> findAllWithDetail(); // Найти контакт со всеми деталями по идентификатору. public ContactEntity findById(Integer id); // Вставить или обновить контакт. public ContactEntity save(ContactEntity contact); // Удалить контакт. public void delete(ContactEntity contact); } |
И его реализация ContactServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package ru.javastudy.impl; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.javastudy.entities.ContactEntity; import ru.javastudy.intf.ContactService; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import java.util.List; @Repository @Service("jpaContactService") @Transactional public class ContactServiceImpl implements ContactService { @PersistenceContext private EntityManager em; public List<ContactEntity> findAll() { return em.createNamedQuery("ContactEntity.findAll", ContactEntity.class).getResultList(); } public List<ContactEntity> findAllWithDetail() { return em.createNamedQuery("ContactEntity.findAllWithDetail", ContactEntity.class).getResultList(); } public ContactEntity findById(Integer id) { TypedQuery<ContactEntity> query = em.createNamedQuery("ContactEntity.findById", ContactEntity.class); query.setParameter("id", id); return query.getSingleResult(); } public ContactEntity save(ContactEntity contact) { return null;} public void delete(ContactEntity contact) {return null;} |
Как видите реализованы только три метода, в которых вызываются именованные запросы из класса ContactEntity.
- @Service — предназначена для идентификации класса как компонента Spring, который предоставляет бизнес-службы другому уровню; этот бин Spring получает имя jpaContactService.
- @Repository указывает, что класс содержит логику доступа к данным и заставляет Spring транслировать исключения, специфичные для поставщика, в иерархию DataAccessException, определенную Spring. В действительности можно использовать @Repository(«jpaContactService») и избавиться от аннотации @Service; результат будет тем же самым.
- @Transactional предназначена для определения требований к транзакции.
Отметьте себе как мы получаем доступ к диспетчеру сущностей, интерфейсу EntityManager — с помощью аннотации @PersistenceContext.
9. Запрашивание данных с использованием языка запросов постоянства Java (JPQL)
Чтобы было понятно о каких данных идет речь, приведу скрины схемы:
Тест проводится в главном классе с парочкой методов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package ru.javastudy.app; import org.springframework.context.support.GenericXmlApplicationContext; import ru.javastudy.entities.ContactEntity; import ru.javastudy.entities.ContactTelDetailEntity; import ru.javastudy.entities.HobbyEntity; import ru.javastudy.intf.ContactService; import java.util.List; public class Main { public static void main(String[] args) { GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); ctx.load("classpath:spring-config.xml"); ctx.refresh(); ContactService contactService = ctx.getBean("jpaContactService", ContactService.class); List<ContactEntity> contacts = contactService.findAll(); printAll(contacts); List<ContactEntity> cotactsWithDetail = contactService.findAllWithDetail(); testContactDetail(cotactsWithDetail); } private static void printAll(List<ContactEntity> contacts) { System.out.println("JPA tutorial. Contact"); for (ContactEntity contact : contacts) { System.out.println(contact); } } private static void testContactDetail(List<ContactEntity> contacts) { System.out.println("Contact with detail"); for (ContactEntity contact : contacts) { System.out.println("Contact: "); System.out.println(contact); if (contact.getContactTelDetails() != null) { for (ContactTelDetailEntity tel : contact.getContactTelDetails()) { System.out.println("Details: "); System.out.println(tel); } } System.out.println("Hobby: "); if (contact.getHobbies() != null) { for (HobbyEntity hobby : contact.getHobbies()) { System.out.println(hobby); } } } } } |
Сначала идет загрузка конфигурации спринг. Затем получаем доступ к интерфейсу ContactService. Далее просто вытаскиваем данные и выводим их в консоль.
Замечание
Вначале статьи писал о местонахождении файла spring-context.xml. Пока не перенес его в папку resources, spring отказывался грузить настройки в ctx.load(‘ ‘), даже указывая абсолютный путь C:\…\spring-config.xml. Кстати на stackoverflow написано с точностью до наоборот — не помещать настройки в папку resources. Кто знает в чем разница — можете написать в комментариях.
Результаты вывода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
JPA tutorial. Contact ContactEntity{id=3, Name0', LastName0', 2015-07-08, 0} ContactEntity{id=8, Name1 ', LastName1', 2014-09-01, 0} ContactEntity{id=10, Name2', LastName2', 2014-05-01, 0} ContactEntity{id=11, Name3', LastName3', 2016-09-02, 0} ContactEntity{id=12, Name4', LastName4', 2015-11-01, 0} ContactEntity{id=13, Name5', LastName5', 2012-09-07, 0} Contact with detail Contact: ContactEntity{id=3, Name0', LastName0', 2015-07-08, 0} Details: ContactTelDetailEntity{version=0, telNumber='555-44-33', telType='Рабочий', id=3} Details: ContactTelDetailEntity{version=0, telNumber='8-800-200-600', telType='Домашний', id=1} Hobby: Contact: ContactEntity{id=8, Name1 ', LastName1', 2014-09-01, 0} Hobby: HobbyEntity{hobbyId='Swimming'} HobbyEntity{hobbyId='Football'} Contact: ContactEntity{id=10, Name2', LastName2', 2014-05-01, 0} Hobby: Contact: ContactEntity{id=11, Name3', LastName3', 2016-09-02, 0} Hobby: HobbyEntity{hobbyId='Movies'} Contact: ContactEntity{id=12, Name4', LastName4', 2015-11-01, 0} Hobby: Contact: ContactEntity{id=13, Name5', LastName5', 2012-09-07, 0} Hobby: HobbyEntity{hobbyId='Football'} |
Для контакта id=3 получили два телефонных номера, 0 хобби.
Для контакта с id=8 получили парочку хобби. И т.д.
Update:
Поменял Long на Integer в методе findById(Integer id), т.к. это соответствует БД:
1 2 3 4 5 |
public ContactEntity findById(Integer id) { TypedQuery<ContactEntity> query = em.createNamedQuery("ContactEntity.findById", ContactEntity.class); query.setParameter("id", id); return query.getSingleResult(); } |
Сначало создаем запрос TypedQuery<T> query с помощью вызова именованного запроса createNamedQuery(<String name, Class<T> resultClass>). Затем устанавливаем параметр в этом запросе и вызываем одиночный результат.
1 2 3 4 5 6 7 |
public class Main { public static void main(String[] args) { ContactEntity contactById = contactService.findById(8); System.out.println(contactById); } } |
Результат:
ContactEntity{id=8, Name1 ‘, LastName1’, 2014-09-01, 0}
Частые ошибки
Если вы попытаетесь обратиться в одном из методов к какой-либо таблице из один-ко-многим или многие-ко-многим, то получите ошибку:
Exception in thread «main» org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: ru.javastudy.entities.ContactEntity.contactTelDetails
Например такую ошибку можно получить просто обратившись к деталям контакта из метода toString():
1 2 3 4 5 6 7 8 9 10 |
public String toString() { return "ContactEntity{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", birthDate=" + birthDate + ", version=" + version + ", contactTelDetails=" + contactTelDetails + //вот здесь обращение и далее ошибка '}'; } |
Почему и как это исправить описано здесь.
Исходные коды
JPA Tutorial — исходные коды
JPA sql helloWorld — дамп базы данных из статьи
107 thoughts on “JPA — пример приложения Hello World. Настройка использования Spring + JPA + Hibernate. Часть 2”
Добавить комментарий
Для отправки комментария вам необходимо авторизоваться.
В чем отличие между schema и catalog?
И еще такой вопрос: сущности, приведенные в этой статье, были сгенерированы полностью автоматически идеей? Или же вручную редактировалось? А то у меня IntelliJ IDEA 2016 какую-то дичь генерирует. Даже связь Many-To-Many не распознает…. Хотя БД та же самая…
Очень тяжело читать код, поля в перемешку с getters setters
contact_tel_detail: дважды использована сущность ContactEntity с разными переменными
если указывать над классом сущности @Table(name = «hobby», schema = «», catalog = «javastudy») то будет warning в (@Column(name = !!!~~~hobby_id~~~~!!!!, nullable = false, length = 20)
)от IDEA (Cannot resolve column ‘hobby_id’ less… (Ctrl+F1) This inspection controls whether the Persistence ORM annotations are checked against configured Datasources), по этому желательно писать @Table(name = «hobby», schema = «javastudy», catalog = «»)
В статье не прописан листинг класса ContactHobbyDetailEntityPK и важное заполнение аннотации
А зачем писать в классе Main -> «jpaContactService» в ContactService contactService = ctx.getBean(«jpaContactService», ContactService.class);
Все и без «jpaContactService» работает замечательно