Hibernate: Пользовательские типы в hibernate. Разбираемся с UDT
Материал из Dom.
Я продолжаю рассказывать об поддержке hibernate пользовательских типов данных и от рассмотрения компонентов, я перехожу, собственно, к созданию собственных типов данных, корректно интегрирующихся в среду hibernate.
Существует несколько интерфейсов являющихся базовыми точками расширения hibernate-функциональности: UserType, CompositeUserType, UserCollectionType, EnhancedUserType, UserVersionType, ParametrizedType. Не все эти интерфейсы часто используются в практике, так я сосредоточусь на описании возможностей только UserType, CompositeUserType и ParametrizedType.
Тем кто дружит с ibatis: Надо сказать, что по сравнению с тем как реализована система пользовательских типов данных в ibatis, hibernate кажется очень, очень громоздким и избыточным. Фактически, казалось бы достаточно создать два метода выполняющих пребразование из стандартного sql-типа данных в java-класс и обратно. Однако, нет: в самом простом случае для интерфейса UserType нам нужно реализовать 11 методов. Все дело в том, что система типов в hibernate более гибкая и универсальная: там мы можем работать с пользовательскими типами данных, которые ну уровне БД отображаются не одним sql-полем, а несколькими. Кроме того, есть возможность создать такой свой тип данных, что можно использовать его при написании hql-запросов.
Для примера я создал копию показанного в прошлой части статьи класса AddressComponent, только назвал его AddressType и теперь буду учить hibernate работать с этим новым типом данных.
Сначала я создаю класс, которых инкапсулирует в себе все поля образующие адрес. Обязательным в составе этого класса является реализация методов hashCode и equals.
package test.db2.model; import java.io.Serializable; public class AddressType implements Serializable { protected String country; protected String city; protected String home; protected String phone; public AddressType() { } public AddressType(String country, String city, String home, String phone) { this.country = country; this.city = city; this.home = home; this.phone = phone; } // обязательно нужно реализовать методы hashCode и equals public int hashCode() { int s = 0; if (country != null) s += country.hashCode(); if (city != null) s = s * 7 + city.hashCode(); if (home != null) s = s * 7 + home.hashCode(); if (phone != null) s = s * 7 + phone.hashCode(); return s; } public boolean equals(Object obj) { if (obj == null) return false; if (!(obj instanceof AddressType)) return false; if (this == obj) return true; AddressType at = (AddressType) obj; return (country == null ? at.country == null : country.equals(at.country)) && (city == null ? at.city == null : city.equals(at.city)) && (home == null ? at.home == null : home.equals(at.home)) && (phone == null ? at.phone == null : phone.equals(at.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; } }
Естественно, что только одного класса AddressType будет малова-то: нам нужен еще один класс, знающий как выполнять преобразование некоторого набора полей hibernate (внимание: преобразование может идти и между не совпадающим количеством полей в таблице БД и количество свойств в составе класса). Итак, я создаю класс AddressTypeDescriptor, в состав которого вводится целая пачка методов для сохранения, восстановления объектов заданного типа и прочая и прочая:
package test.db2.model; import org.hibernate.usertype.UserType; import org.hibernate.HibernateException; import org.hibernate.Hibernate; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.PreparedStatement; import java.io.Serializable; public class AddressTypeDescriptor implements UserType { // т.к. мой старый новый тип данных адреса состоит из четырех текстовых полей, то здесь я и задам массив // с указанием их тиво данных int [] sqlTypes = new int [] { Hibernate.STRING.sqlType(), Hibernate.STRING.sqlType(), Hibernate.STRING.sqlType(), Hibernate.STRING.sqlType() }; /** * Возвращаем массив с указанием того какие типы данных образуют данное "высокоуровневое свойство" * @return */ public int[] sqlTypes() { return sqlTypes; } /** * Теперь нужно сообщить о том, какой тип данных используется для хранения адреса * @return */ public Class returnedClass() { return AddressType.class; } /** * Сравнение двух объектов заданного типа данных (после простейших проверок эту задачу можно делегировать методу equals в составе AddressType) * @param x * @param y * @return * @throws HibernateException */ public boolean equals(Object x, Object y) throws HibernateException { if (x == null || y == null) return false; return x.equals(y); } /** * Просто перевызываем метод hashCode для целевого объекта x * @param x * @return * @throws HibernateException */ public int hashCode(Object x) throws HibernateException { if (x == null) return 0; return x.hashCode(); } public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException { AddressType at = new AddressType(); at.setCountry(rs.getString(names[0])); at.setCity(rs.getString(names[1])); at.setHome(rs.getString(names[2])); at.setPhone(rs.getString(names[3])); return at; } public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException { AddressType at = (AddressType) value; if (at != null){ st.setString(index+0, at.getCountry()); st.setString(index+1, at.getCity()); st.setString(index+2, at.getHome()); st.setString(index+3, at.getPhone()); } else{ st.setNull(index+0, sqlTypes[0]); st.setNull(index+1, sqlTypes[1]); st.setNull(index+2, sqlTypes[2]); st.setNull(index+3, sqlTypes[3]); } } /** * Здесь нужно создать копию объекта (полную, или глубокую копию) * @param value * @return * @throws HibernateException */ public Object deepCopy(Object value) throws HibernateException { if (value == null) return null; AddressType at = (AddressType) value; return new AddressType (at.getCountry(), at.getCity(), at.getHome(), at.getPhone()); } /** * Да, мы обладаем способностью к изменению * @return */ public boolean isMutable() { return true; } /** * Этот метод используется когда нужно объект поместить внутрь cache второго уровня * @param value * @return * @throws HibernateException */ public Serializable disassemble(Object value) throws HibernateException { return (Serializable) value; } /** * А здесь обратный процесс, когда объект восстанавливается из сериализованного представления себя * @param cached * @param owner * @return * @throws HibernateException */ public Object assemble(Serializable cached, Object owner) throws HibernateException { return cached; } /** * Метод вызывается когда необходимо выполнить свлияние двух объектов * @param original * @param target * @param owner * @return * @throws HibernateException */ public Object replace(Object original, Object target, Object owner) throws HibernateException { return deepCopy(original); } }
Теперь разберем вкратце методы образующие класс:
1. sqlTypes этот метод должен вернуть массив целых чисел - кодов. Т.е. мы должны сообщить hibernate о том, какие типы данных полей необходимы для хранения полей класса пользовательского типа данных. Т.к. мой класс AddressType содержит четырые строковые поля, то я и возвращаю массив из четырех кодов Hibernate.STRING.sqlType().
2. returnedClass код этого метода прозрачен - мы должны вернуть hibernate информацию о том какой тип данных представляется в java.
3. equals метод служит для сравнения двух объектов на равенство (например, при выполнении проверки на "чистоту" при закрытии сессии)
4. hashCode - без комментариев.
5. метод nullSafeGet служит для того, чтобы выполнить чтение из базы данных содержимого моего типа UserAddress. В качестве параметров методу передается объект ResultSet, однако для того чтобы мы могли читать данные из resultset-а, нам нужно знать либо имена полей, либо их порядковые номера. Таким образом hibernate передает внутрь метода nullSafeGet еще и массив String[] names с именами полей.
6. метод nullSafeSet выполняет парное действие к nullSafeGet и служит для записи содержимого объекта внутрь БД.
7. deepCopy метод должен выполнить глубокое клонирование некоторого объекта.
8. результатом вызова метода isMutable будет boolean, говорящий о том является ли данный тип данных (AddressType) изменяемым после своего создания или нет.
9. методы disassemble и disassemble служат для выполнения корректной сериализации объекта при передаче его между hibernate session и кэшем второго уровня.
10. метод replace вызывается при выполнении операции merge записи с существующей в БД.
Изменения в классе User тривиальны: я избавился от ненужных полей в составе класса и заменил AddressComponent на AddressType:
@Entity public class User { @Id @GeneratedValue protected Integer id; protected String fio; protected AddressType homeAddress; public User() { } public User(String fio, AddressType homeAddress) { this.fio = fio; this.homeAddress = homeAddress; } 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 AddressType getHomeAddress() { return homeAddress; } public void setHomeAddress(AddressType homeAddress) { this.homeAddress = homeAddress; } }
Теперь необходимо выполнить правки в xml-файле маппинга для класса User: нам нужно также как и при работе с компонентами указать какие поля в таблице БД будут образовывать наш тип данных.
<hibernate-mapping package="test.db2.model"> <class name="User"> <id name="id" type="int"> <generator class="native" /> </id> <property name="fio" type="string" /> <property name="homeAddress" type="test.db2.model.AddressTypeDescriptor" > <column name="country" /> <column name="city" /> <column name="home" /> <column name="phone" /> </property> </class> </hibernate-mapping>
Никаких секретов при работе с свежесозданным типом данных нет. Так что сразу перейду к рассмотрению того как можно решить задачу настройки маппинга, но через аннотации. Для работы я использую аннотацию @Type, обратите внимание на то, что это специфическая для hibernate аннотация. Важно указать не имя класса типа данных AddressType, а имя класса дескриптора для этого класса AddressTypeDescriptor. Кроме того с помощью аннотации @Columns (опять это аннотация специфична, именно, для hibernate) вы можете указать имена полей в таблице БД, на которую выполняется отображение данного типа данных:
@org.hibernate.annotations.Type(type = "test.db2.model.AddressTypeDescriptor") @Columns(columns = { @Column(name = "home_country"), @Column(name = "home_city"), @Column(name = "home_home"), @Column(name = "home_phone") }) protected AddressType homeAddress;
Что касается возможности хранения внутри пользовательского типа данных, сложных структур данных, например, коллекций, ассоциаций с другими сущностями, то этого сделать нельзя.
Показанный выше пример создал класс дескриптора нового типа данных как реализующий интерфейс UserType. Теперь попробуем расширить класс AddressTypeDescriptor и заставим его реализовать интерфейс CompositeUserType. Если посмотреть на то, какие методы образуют данный интерфейс, то можете увидеть что многие методы перекочевали из UserType, часть методов при этом немного изменила сигнатуры, чтобы соответствовать новым требованиям. Самый главный вопрос: зачем нужен CompositeUserType и чем он отличается от UserType? Все дело в том, что когда мы создаем новый тип данных, то фактически приводим к созданию новых свойств не однозначно связанных с полями исходной таблицы. Это не составило бы больших проблем, если бы мы ограничились простыми действиями, вроде "создать запись, сохранить, удалить". Однако, когда мы говорим о поиске данных, то возникает проблема: я хочу сформирулировать запрос "найти всех пользователей, которые живут в РБ". Для нас очевидно, что так сконструировать запрос, чтобы он сравнивал значение поля "home_country" со значением "РБ". Однако, чтобы сделать так hibernate должен знать об внутреннем устройстве пользовательского типа данных немного больше, чем то, какие типы полей его образуют, давайте дадим ему это:
package test.db2.model; import org.hibernate.usertype.UserType; import org.hibernate.usertype.CompositeUserType; import org.hibernate.HibernateException; import org.hibernate.Hibernate; import org.hibernate.engine.SessionImplementor; import org.hibernate.type.Type; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.PreparedStatement; import java.io.Serializable; public class AddressTypeDescriptor implements CompositeUserType { /** * возвращаем информацию об том какие имена полей образуют новый тип данных * * @return */ public String[] getPropertyNames() { return new String[]{"country", "city", "home", "phone"}; } /** * возвращаем информацию о том, какие типы полей образуют эти свойства * * @return */ public Type[] getPropertyTypes() { return new Type[]{ Hibernate.STRING, Hibernate.STRING, Hibernate.STRING, Hibernate.STRING }; } /** * теперь надо на основании порядкового номера свойства вернуть его значение * * @param component * @param property * @return * @throws HibernateException */ public Object getPropertyValue(Object component, int property) throws HibernateException { AddressType at = (AddressType) component; switch (property) { case 0: return at.getCountry(); case 1: return at.getCity(); case 2: return at.getHome(); case 3: return at.getPhone(); default: throw new IllegalArgumentException("invalid property numer '" + property + "'"); } } /** * теперь выполняем парную операцию, устанавливая новое значение для сложного свойства * * @param component * @param property * @param value * @throws HibernateException */ public void setPropertyValue(Object component, int property, Object value) throws HibernateException { AddressType at = (AddressType) component; switch (property) { case 0: at.setCountry((String) value); break; case 1: at.setCity((String) value); break; case 2: at.setHome((String) value); break; case 3: at.setPhone((String) value); break; default: throw new IllegalArgumentException("invalid property numer '" + property + "'"); } } /** * указываем сведения об том типе данных, который обслуживает данный класс * * @return */ public Class returnedClass() { return AddressType.class; } /** * Сравнение двух объектов заданного типа данных (после простейших проверок * эту задачу можно делегировать методу equals в составе AddressType) * * @param x * @param y * @return * @throws HibernateException */ public boolean equals(Object x, Object y) throws HibernateException { if (x == null || y == null) return false; return x.equals(y); } /** * Просто перевызываем метод hashCode для целевого объекта x * * @param x * @return * @throws HibernateException */ public int hashCode(Object x) throws HibernateException { if (x == null) return 0; return x.hashCode(); } /** * нужно выполнить чтение данных образующих свойство из ResultSet-а * * @param rs * @param names * @param session * @param owner * @return * @throws HibernateException * @throws SQLException */ public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { AddressType at = new AddressType(); at.setCountry(rs.getString(names[0])); at.setCity(rs.getString(names[1])); at.setHome(rs.getString(names[2])); at.setPhone(rs.getString(names[3])); return at; } public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { AddressType at = (AddressType) value; if (at != null) { st.setString(index + 0, at.getCountry()); st.setString(index + 1, at.getCity()); st.setString(index + 2, at.getHome()); st.setString(index + 3, at.getPhone()); } else { st.setNull(index + 0, Hibernate.STRING.sqlType()); st.setNull(index + 1, Hibernate.STRING.sqlType()); st.setNull(index + 2, Hibernate.STRING.sqlType()); st.setNull(index + 3, Hibernate.STRING.sqlType()); } } /** * Здесь нужно создать копию объекта (полную, или глубокую копию) * @param value * @return * @throws HibernateException */ public Object deepCopy(Object value) throws HibernateException { if (value == null) return null; AddressType at = (AddressType) value; return new AddressType (at.getCountry(), at.getCity(), at.getHome(), at.getPhone()); } /** * Да, мы обладаем способностью к изменению * @return */ public boolean isMutable() { return true; } /** * Этот метод используется когда нужно объект поместить внутрь cache второго уровня * @param value * @param session * @return * @throws HibernateException */ public Serializable disassemble(Object value, SessionImplementor session) throws HibernateException { return (Serializable)value; } /** * А здесь обратный процесс, когда объект восстанавливается из сериализованного представления себя * @param cached * @param session * @param owner * @return * @throws HibernateException */ public Object assemble(Serializable cached, SessionImplementor session, Object owner) throws HibernateException { return cached; } /** * Метод вызывается когда необходимо выполнить свлияние двух объектов * @param original * @param target * @param session * @param owner * @return * @throws HibernateException */ public Object replace(Object original, Object target, SessionImplementor session, Object owner) throws HibernateException { return deepCopy(original); } }
Изменений не много: прежде всего добавлены методы, которые позволяют hibernate узнать об логических именах полей образующих пользовательский тип данных (именно, логических). Также появилась пара методов getPropertyValue и setPropertyValue, служащих для изменения значения отдельного значения свойства. Будьте внимательны и во всех методах контролируйте правильный порядок полей свойства.
Теперь попробуем поиграться с новым типом данных для создания hql-запроса (равно, как и criteria-основанного запроса) для поиска данных в БД. Для этого мне пришлось внести очередные правки в класс User (также помимо добавления поля homeAddress хранящего сведения об домашнем адресе пользователя в виде нового типа данных), я вернул в состав User-а информацию об его рабочем адресе (но только в форме рассмотренного в прошлой статье component-а):
package test.db2.model; import org.hibernate.annotations.AccessType; import org.hibernate.annotations.Columns; import javax.persistence.*; import java.io.Serializable; @Entity public class User { @Id @GeneratedValue protected Integer id; protected String fio; @org.hibernate.annotations.Type(type = "test.db2.model.AddressTypeDescriptor") @Columns(columns = { @Column(name = "home_country"), @Column(name = "home_city"), @Column(name = "home_home"), @Column(name = "home_phone") }) protected AddressType homeAddress; @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")) }) @Embedded protected AddressComponent workAddress; @ManyToOne (cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn (name = "fk_department_id") protected Department department; public User() { } public User(String fio, AddressType homeAddress) { this.fio = fio; this.homeAddress = homeAddress; } 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 AddressType getHomeAddress() { return homeAddress; } public void setHomeAddress(AddressType homeAddress) { this.homeAddress = homeAddress; } public AddressComponent getWorkAddress() { return workAddress; } public void setWorkAddress(AddressComponent workAddress) { this.workAddress = workAddress; } public Department getDepartment() { return department; } public void setDepartment(Department department) { this.department = department; } }
Теперь создадим несколько записей в БД:
Session session = getSession(); session.beginTransaction(); User vasyano = new User("Vasyano Tapkin", new AddressType("Республика Беларусь", "минск", "13", "1234567890")); User petyano = new User("Petyano Gromov", new AddressType("belarus", null, null, null)); User lenka = new User("Lenka Umkina", null); lenka.setWorkAddress(new AddressComponent(new Country("belarus", "bla-bla-bla"), "minsk", "13", "1234567890")); session.saveOrUpdate(vasyano); session.saveOrUpdate(petyano); session.saveOrUpdate(lenka); session.getTransaction().commit();
Теперь попробуем искать данные в БД. Сначала используем запрос на hql, который ищет тех людей, которые проживают (т.е. говорим об домашнем адресе) в Республике Беларусь (belarus):
Query hql_query = session.createQuery("from User where homeAddress.country = :country"); hql_query.setString("country", "belarus"); List hql_list_in_rb = hql_query.list();
Как видите, никаких проблем: я могу обращаться к сложному и логическому имени (homeAddress.country), ведь hibernate знает о том из каких именно полей состоит созданный мною тип данных AddressType.
Второй пример запроса так же работает с hql и домашним адресом клиента. Однако, в этом случае я хочу выполнить поиск не на основании значения отдельных свойств домашнего адреса, а сравнить весь домашний адрес: