lenka2 = null petyano2 = null vasyano2 = test.db2.model.AddressComponent@e45b5e
« Пишем и тестируем код, работающий с БД, вместе с DBUnit и LiquiBase. Часть 1 | Hibernate: Пользовательские типы в hibernate. Разбираемся с UDT » |
Hibernate: Пользовательские типы в hibernate. Разбираемся с компонентами
Тема сегодняшней статьи - пользовательские типы в hibernate. Разбираемся с компонентами.Hibernate служит для отображения java-классов на таблицы БД. Естественно, что бывают ситуации, когда иерархия (сеть) классов java является более "богатой" и не может быть (не должна) переводиться в таблицы БД непосредственно. Классический пример, это класс (таблица) User, который помимо простых свойств fio, age (отображаемых непосредственно на поля таблицы user), содержит более сложные поля. Например, поле homeAddress (тип Address) хранит сведения об домашнем адресе user-а. Можно было бы создать для класса Address собственный mapping и связать классы user & address с помощью ассоциации "один-к-одному", однако это несколько не красиво с точки зрения здравого смысла. Ведь все классы, отображаемые на таблицы, имеют собственный идентификатор и могут быть задействованы в запросах вида:
Session s = SessionFactory.openSession ();
s.get (model.db.Address.class, 12);
s.createQuery ('from Address where ...').list ()
Компонент - это java класс, который внедряется в состав другого класса. Например, класс AddressComponent представляет собой свойство homeAddress в составе класса User.
package test.db2.model;
public class AddressComponent {
protected String country;
protected String city;
protected String home;
protected String phone;
protected User homeOwner;
public AddressComponent() {
}
public AddressComponent(String country, String city, String home, String phone) {
this.country = country;
this.city = city;
this.home = home;
this.phone = phone;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getHome() {
return home;
}
public void setHome(String home) {
this.home = home;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public User getHomeOwner() {
return homeOwner;
}
public void setHomeOwner(User homeOwner) {
this.homeOwner = homeOwner;
}
}
package test.db2.model;
public class User {
protected Integer id;
protected String fio;
protected AddressComponent homeAddress;
public User() {}
public User(String fio, AddressComponent homeAddress) {
this.fio = fio;
this.homeAddress = homeAddress;
if (homeAddress != null)
homeAddress.setHomeOwner(this);
// *** внимание, это нужно для корректной работы с transient-объектами
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getFio() {
return fio;
}
public void setFio(String fio) {
this.fio = fio;
}
public AddressComponent getHomeAddress() {
return homeAddress;
}
public void setHomeAddress(AddressComponent homeAddress) {
this.homeAddress = homeAddress;
if (homeAddress != null)
homeAddress.setHomeOwner(this);
// *** внимание, это нужно для корректной работы с transient-объектами
}
}
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="test.db2.model">
<class name="User">
<id name="id" type="int">
<generator class="native" />
</id>
<property name="fio" type="string" />
<component name="homeAddress" class="AddressComponent" >
<parent name="homeOwner" />
<property name="country" />
<property name="city" />
<property name="home" />
<property name="phone" />
</component>
</class>
</hibernate-mapping>
public void setHomeAddress(AddressComponent homeAddress) {
this.homeAddress = homeAddress;
if (homeAddress != null)
homeAddress.setHomeOwner(this); // ***
}
mysql> SHOW CREATE TABLE user;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fio` varchar(255) DEFAULT NULL,
`country` varchar(255) DEFAULT NULL,
`city` varchar(255) DEFAULT NULL,
`home` varchar(255) DEFAULT NULL,
`phone` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
protected AddressComponent workAddress;
<hibernate-mapping package="test.db2.model">
<class name="User">
<id name="id" type="int">
<generator class="native" />
</id>
<property name="fio" type="string" />
<component name="homeAddress" class="AddressComponent" >
<parent name="homeOwner" />
<property name="country" column="home_country" />
<property name="city" column="home_city" />
<property name="home" column="home_home" />
<property name="phone" column="home_phone" />
</component>
<component name="workAddress" class="AddressComponent" >
<parent name="homeOwner" />
<property name="country" column="work_country" />
<property name="city" column="work_city" />
<property name="home" column="work_home" />
<property name="phone" column="work_phone" />
</component>
</class>
</hibernate-mapping>
public static void main(String[] args) {
Session session = getSession();
session.beginTransaction();
User vasyano = new User("Vasyano Tapkin", new AddressComponent("belarus", "minsk", "13", "1234567890"), null);
User petyano = new User("Petyano Gromov", new AddressComponent("belarus", "vitebsk", "14", "0987654321"), null);
User lenka = new User("Lenka Umkina", new AddressComponent("belarus", "grodno", "15", "1029384756"), null);
lenka.setWorkAddress(null);
petyano.setWorkAddress(new AddressComponent(null, null, null, null));
vasyano.setWorkAddress(new AddressComponent(null, null, "13", null));
session.saveOrUpdate(vasyano);
session.saveOrUpdate(petyano);
session.saveOrUpdate(lenka);
session.getTransaction().commit();
session = getSession();
session.beginTransaction();
User lenka2 = (User) session.load(User.class, lenka.getId());
User petyano2 = (User) session.load(User.class, petyano.getId());
User vasyano2 = (User) session.load(User.class, vasyano.getId());
System.out.println("lenka2 = " + lenka2.getWorkAddress());
System.out.println("petyano2 = " + petyano2.getWorkAddress());
System.out.println("vasyano2 = " + vasyano2.getWorkAddress());
session.getTransaction().commit();
}
Теперь рассмотрим то, как можно работать с компонентами, используя не файлы xml-маппинга, а, именно, аннотации:
Прежде всего я должен аннотировать класс AddressComponent с помощью аннотации @Embeddable (т.е. способный к работе в роли компонента).
package test.db2.model;
import org.hibernate.annotations.Parent;
import javax.persistence.Embedded;
import javax.persistence.Embeddable;
@Embeddable
public class AddressComponent {
protected String country;
protected String city;
protected String home;
protected String phone;
@Parent
protected User homeOwner;
public AddressComponent() {
}
public AddressComponent(String country, String city, String home, String phone) {
this.country = country;
this.city = city;
this.home = home;
this.phone = phone;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getHome() {
return home;
}
public void setHome(String home) {
this.home = home;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public User getHomeOwner() {
return homeOwner;
}
public void setHomeOwner(User homeOwner) {
this.homeOwner = homeOwner;
}
}
@Entity
public class User {
@Id
@GeneratedValue
protected Integer id;
protected String fio;
@Embedded
protected AddressComponent homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "country", column = @Column(name = "work_country")),
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "home", column = @Column(name = "work_home")),
@AttributeOverride(name = "phone", column = @Column(name = "work_phone"))
})
protected AddressComponent workAddress;
... все как раньше ...
package test.db2.model;
import javax.persistence.Embeddable;
@Embeddable
public class Country {
protected String name;
protected String anthem;
public Country() {
}
public Country(String name, String anthem) {
this.name = name;
this.anthem = anthem;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAnthem() {
return anthem;
}
public void setAnthem(String anthem) {
this.anthem = anthem;
}
}
package test.db2.model;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@Entity
public class Furniture {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
protected String name;
public Furniture() {
}
public Furniture(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package test.db2.model;
import org.hibernate.annotations.Parent;
import javax.persistence.*;
import java.util.List;
import java.util.ArrayList;
@Embeddable
public class AddressComponent {
@Embedded
protected Country country;
protected String city;
protected String home;
protected String phone;
@Parent
protected User homeOwner;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
protected List<Furniture> furniture = new ArrayList<Furniture>();
public AddressComponent() {
}
public AddressComponent(Country country, String city, String home, String phone) {
this.country = country;
this.city = city;
this.home = home;
this.phone = phone;
}
public Country getCountry() {
return country;
}
public void setCountry(Country country) {
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getHome() {
return home;
}
public void setHome(String home) {
this.home = home;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public User getHomeOwner() {
return homeOwner;
}
public void setHomeOwner(User homeOwner) {
this.homeOwner = homeOwner;
}
public List<Furniture> getFurniture() {
return furniture;
}
public void setFurniture(List<Furniture> furniture) {
this.furniture = furniture;
}
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Integer id;
protected String fio;
@Embedded
protected AddressComponent homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "country.name", column = @Column(name = "work_country_name")),
@AttributeOverride(name = "country.anthem", column = @Column(name = "work_country_anthem")),
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "home", column = @Column(name = "work_home")),
@AttributeOverride(name = "phone", column = @Column(name = "work_phone"))
})
protected AddressComponent workAddress;
mysql> show tables; +------------------------+ | Tables_in_demoarticles | +------------------------+ | furniture | | user | | user_furniture | +------------------------+ 3 rows in set (0.00 sec) mysql> show create table furniture; +-----------+--------------------------------------------------------------------------------- | Table | Create Table +-----------+--------------------------------------------------------------------------------- | furniture | CREATE TABLE `furniture` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +-----------+--------------------------------------------------------------------------------- 1 row in set (0.00 sec) mysql> show create table user; --------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+------------------------------------------------------------------------------------- | user | CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `fio` varchar(255) DEFAULT NULL, `city` varchar(255) DEFAULT NULL, `anthem` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `home` varchar(255) DEFAULT NULL, `phone` varchar(255) DEFAULT NULL, `work_city` varchar(255) DEFAULT NULL, `work_country_anthem` varchar(255) DEFAULT NULL, `work_country_name` varchar(255) DEFAULT NULL, `work_home` varchar(255) DEFAULT NULL, `work_phone` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +-------+------------------------------------------------------------------------------------- 1 row in set (0.00 sec) mysql> show create table user_furniture; ---------------------------------------------------------------------------------------------- | Table | Create Table +----------------+---------------------------------------------------------------------------- | user_furniture | CREATE TABLE `user_furniture` ( `User_id` int(11) NOT NULL, `furniture_id` int(11) NOT NULL, UNIQUE KEY `furniture_id` (`furniture_id`), KEY `FK7688287E861E29FF` (`furniture_id`), KEY `FK7688287E931FA535` (`User_id`), CONSTRAINT `FK7688287E931FA535` FOREIGN KEY (`User_id`) REFERENCES `user` (`id`), CONSTRAINT `FK7688287E861E29FF` FOREIGN KEY (`furniture_id`) REFERENCES `furniture` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +----------------+---------------------------------------------------------------------------- 1 row in set (0.00 sec)
Session session = getSession();
session.beginTransaction();
Country belarus = new Country("belarus", "bla-bla-bla");
User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
lenka.setWorkAddress(new AddressComponent(null, null, null, null));
petyano.setWorkAddress(new AddressComponent(null, null, null, null));
vasyano.setWorkAddress(new AddressComponent(null, null, "13", null));
Furniture table = new Furniture("table");
Furniture chair = new Furniture("chair");
Furniture armchair = new Furniture("armchair");
lenka.getHomeAddress().getFurniture().add(table);
lenka.getHomeAddress().getFurniture().add(chair);
lenka.getHomeAddress().getFurniture().add(armchair);
//lenka.getWorkAddress().getFurniture().add(table); -- это не будет работать, догадайтесь сами почему?
lenka.getWorkAddress().getFurniture().add(new Furniture("table"));
session.saveOrUpdate(vasyano);
session.saveOrUpdate(petyano);
session.saveOrUpdate(lenka);
session.getTransaction().commit();
session = getSession();
session.beginTransaction();
User lenka2 = (User) session.get(User.class, lenka.getId());
User petyano2 = (User) session.get(User.class, petyano.getId());
User vasyano2 = (User) session.get(User.class, vasyano.getId());
System.out.println("lenka2 = " + lenka2.getWorkAddress());
System.out.println("petyano2 = " + petyano2.getWorkAddress());
System.out.println("vasyano2 = " + vasyano2.getWorkAddress());
session.getTransaction().commit();
//lenka.getWorkAddress().getFurniture().add(table); -- это не будет работать, догадайтесь сами почему?
lenka.getWorkAddress().getFurniture().add(new Furniture("table"));
Как вывод: сам hibernate не может разобраться с тем как настроить связи для пары компонент-список элементов, и мы должны ему подсказать об этом. Формально для решения этой проблемы предназначена аннотация @AssociationOverride. Однако я так и не смог заставить ее работать, так что если кто-то знает и умеет больше, пожалуйста, напишите мне - буду благодарен.
Завершая рассказ об Embedded или компонентах, я расскажу об такой редкой (а может совсем и не редкой, если вы работаете с унаследованными БД) ситуации, когда компонент служит для представления не обычного поля в таблице, а играет роль первичного ключа этой таблицы. Для этого я в состав класса User ввожу вложенный public static класс UserID. Натуральный композитный первичный ключ теперь будет образован двумя полями: серия паспорта и номер паспорта. Из обязательных требований к композитному id является то, что нужно декларировать сам класс как @Embeddable, реализовать интерфейс Serializable, и (очень, очень, очень важно) перекрыть методы equals и hashCode для класса.
@Entity
public class User {
@Embeddable
@AccessType(value = "field")
public static class UserID implements Serializable {
protected String passport_num;
protected String passport_serie;
public UserID() {}
public UserID(String passport_num, String passport_serie) {
this.passport_num = passport_num;
this.passport_serie = passport_serie;
}
public int hashCode() {
return passport_num.hashCode()*29+passport_serie.hashCode();
}
public boolean equals(Object obj) {
if (obj == null) return false;
if (!(obj instanceof UserID)) return false;
UserID id = (UserID) obj;
return (passport_num == null ? id.passport_num == null : passport_num.equals(id.passport_num))
&& (passport_serie == null ? id.passport_serie == null : passport_serie.equals(id.passport_serie));
}
}
@EmbeddedId
protected UserID id;
Session session = getSession();
session.beginTransaction();
Country belarus = new Country("belarus", "bla-bla-bla");
User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
// теперь нужно явно задать идентификаторы для этих записей
vasyano.setId(new User.UserID("123", "code_a" ));
petyano.setId(new User.UserID("456", "code_a" ));
lenka.setId(new User.UserID("123", "code_b" ));
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Integer id;
protected String name;
@OneToMany(mappedBy = "id.department", cascade = CascadeType.ALL)
@JoinColumn (name = "fk_department_id")
Set<User> users = new HashSet<User>();
public Department() {}
public Department(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
@Embeddable
@AccessType(value = "field")
public static class UserID implements Serializable {
public String passport_num;
public String passport_serie;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
@JoinColumn(name = "fk_department_id")
public Department department;
public UserID() {
}
public UserID(String passport_num, String passport_serie, Department department) {
this.passport_num = passport_num;
this.passport_serie = passport_serie;
this.department = department;
}
public int hashCode() {
return passport_num.hashCode() * 29 + passport_serie.hashCode() * 17 + department.hashCode();
}
public boolean equals(Object obj) {
if (obj == null) return false;
if (!(obj instanceof UserID)) return false;
UserID id = (UserID) obj;
return
(passport_num == null ? id.passport_num == null : passport_num.equals(id.passport_num)) &&
(passport_serie == null ? id.passport_serie == null : passport_serie.equals(id.passport_serie)) &&
(department == null ? id.department == null : department.equals(id.department));
}
}
Session session = getSession();
session.beginTransaction();
Department managers = new Department("managers");
Department designers = new Department("designers");
Country belarus = new Country("belarus", "bla-bla-bla");
User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
session.saveOrUpdate(managers);
session.saveOrUpdate(designers);
// теперь нужно явно задать идентификаторы для этих записей
vasyano.setId(new User.UserID("123", "code_a", managers));
petyano.setId(new User.UserID("456", "code_a", managers));
lenka.setId(new User.UserID("123", "code_b", designers));
session.saveOrUpdate(vasyano);
session.saveOrUpdate(petyano);
session.saveOrUpdate(lenka);
session.getTransaction().commit();
session = getSession();
session.beginTransaction();
User lenka2 = (User) session.get(User.class, lenka.getId());
Department designers_2 = (Department )session.get(Department.class, 2);
session.getTransaction().commit();
Решением этой проблемы может использование другой стратегии работы с композитным первичным ключом. В этом случае я меняю декларацию класса User на следующую:
@Entity
@IdClass (User.UserID.class)
public class User implements Serializable{
@AccessType(value = "field")
public static class UserID implements Serializable {
public String passport_num;
public String passport_serie;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
@JoinColumn(name = "fk_department_id")
public Department department;
public UserID() {
}
public UserID(String passport_num, String passport_serie, Department department) {
this.passport_num = passport_num;
this.passport_serie = passport_serie;
this.department = department;
}
public int hashCode() {
return passport_num.hashCode() * 29 + passport_serie.hashCode() * 17 + department.hashCode();
}
public boolean equals(Object obj) {
if (obj == null) return false;
if (!(obj instanceof UserID)) return false;
UserID id = (UserID) obj;
return
(passport_num == null ? id.passport_num == null : passport_num.equals(id.passport_num)) &&
(passport_serie == null ? id.passport_serie == null : passport_serie.equals(id.passport_serie)) &&
(department == null ? id.department == null : department.equals(id.department));
}
}
@Id
public String passport_num;
@Id
public String passport_serie;
@Id
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
@JoinColumn(name = "fk_department_id")
public Department department;
Небольшие правки были внесены и в состав класса Department:
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Integer id;
protected String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
@JoinColumn (name = "fk_department_id")
Set<User> users = new HashSet<User>();
public Department() {}
public Department(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
Session session = getSession();
session.beginTransaction();
Department managers = new Department("managers");
Department designers = new Department("designers");
Country belarus = new Country("belarus", "bla-bla-bla");
User vasyano = new User("Vasyano Tapkin", new AddressComponent(belarus, "minsk", "13", "1234567890"), null);
User petyano = new User("Petyano Gromov", new AddressComponent(belarus, "vitebsk", "14", "0987654321"), null);
User lenka = new User("Lenka Umkina", new AddressComponent(belarus, "grodno", "15", "1029384756"), null);
session.saveOrUpdate(managers);
session.saveOrUpdate(designers);
// теперь нужно явно задать идентификаторы для этих записей
vasyano.setPassport_serie("code_a");
vasyano.setPassport_num("123");
vasyano.setDepartment(managers);
petyano.setPassport_serie("code_b");
petyano.setPassport_num("455");
petyano.setDepartment(managers);
lenka.setPassport_serie("code_a");
lenka.setPassport_num("321");
lenka.setDepartment(designers);
session.saveOrUpdate(vasyano);
session.saveOrUpdate(petyano);
session.saveOrUpdate(lenka);
session.getTransaction().commit();
session = getSession();
session.beginTransaction();
// нужно обязательно загрузить запись об отделе в рамках текущей сессии
Department designers_2 = (Department )session.get(Department.class, 2);
//а теперь можно загрузить сведения и об человеке
User lenka2 = (User) session.get(User.class, new User.UserID("321", "code_a", designers_2) );
session.getTransaction().commit();
« Пишем и тестируем код, работающий с БД, вместе с DBUnit и LiquiBase. Часть 1 | Hibernate: Пользовательские типы в hibernate. Разбираемся с UDT » |