Hibernate: Пользовательские типы в hibernate. Разбираемся с UDT

July 31, 2008

Я продолжаю рассказывать об поддержке 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.
  1. package test.db2.model;
  2.  
  3.  
  4. import java.io.Serializable;
  5.  
  6.  
  7. public class AddressType implements Serializable {
  8.  
  9.     protected String country;
  10.     protected String city;
  11.     protected String home;
  12.     protected String phone;
  13.  
  14.     public AddressType() {
  15.     }
  16.  
  17.     public AddressType(String country, String city, String home, String phone) {
  18.         this.country = country;
  19.         this.city = city;
  20.         this.home = home;
  21.         this.phone = phone;
  22.     }
  23.  
  24.  
  25.     // обязательно нужно реализовать методы hashCode и equals
  26.     public int hashCode() {
  27.         int s = 0;
  28.         if (country != null) s += country.hashCode();
  29.         if (city != null) s = s * 7 + city.hashCode();
  30.         if (home != null) s = s * 7 + home.hashCode();
  31.         if (phone != null) s = s * 7 + phone.hashCode();
  32.         return s;
  33.     }
  34.  
  35.     public boolean equals(Object obj) {
  36.         if (obj == null) return false;
  37.         if (!(obj instanceof AddressType)) return false;
  38.         if (this == obj) return true;
  39.         AddressType at = (AddressType) obj;
  40.         return (country == null ? at.country == null : country.equals(at.country)) &&
  41.                 (city == null ? at.city == null : city.equals(at.city)) &&
  42.                 (home == null ? at.home == null : home.equals(at.home)) &&
  43.                 (phone == null ? at.phone == null : phone.equals(at.phone));
  44.     }
  45.  
  46.     public String getCountry() {
  47.         return country;
  48.     }
  49.  
  50.     public void setCountry(String country) {
  51.         this.country = country;
  52.     }
  53.  
  54.     public String getCity() {
  55.         return city;
  56.     }
  57.  
  58.     public void setCity(String city) {
  59.         this.city = city;
  60.     }
  61.  
  62.     public String getHome() {
  63.         return home;
  64.     }
  65.  
  66.     public void setHome(String home) {
  67.         this.home = home;
  68.     }
  69.  
  70.     public String getPhone() {
  71.         return phone;
  72.     }
  73.  
  74.     public void setPhone(String phone) {
  75.         this.phone = phone;
  76.     }
  77. }
Естественно, что только одного класса AddressType будет малова-то: нам нужен еще один класс, знающий как выполнять преобразование некоторого набора полей hibernate (внимание: преобразование может идти и между не совпадающим количеством полей в таблице БД и количество свойств в составе класса). Итак, я создаю класс AddressTypeDescriptor, в состав которого вводится целая пачка методов для сохранения, восстановления объектов заданного типа и прочая и прочая:
  1. package test.db2.model;
  2.  
  3. import org.hibernate.usertype.UserType;
  4. import org.hibernate.HibernateException;
  5. import org.hibernate.Hibernate;
  6.  
  7. import java.sql.ResultSet;
  8. import java.sql.SQLException;
  9. import java.sql.PreparedStatement;
  10. import java.io.Serializable;
  11.  
  12. public class AddressTypeDescriptor implements UserType {
  13.  
  14.     // т.к. мой старый новый тип данных адреса состоит из четырех текстовых полей, 
  15.     // то здесь я и задам массив с указанием их тиво данных
  16.     int [] sqlTypes = new int [] {
  17.             Hibernate.STRING.sqlType(), Hibernate.STRING.sqlType(), 
  18.             Hibernate.STRING.sqlType(), Hibernate.STRING.sqlType()
  19.     };
  20.  
  21.     /**
  22.      * Возвращаем массив с указанием того какие типы данных образуют данное "высокоуровневое свойство"
  23.      * @return
  24.      */
  25.     public int[] sqlTypes() {
  26.         return sqlTypes;
  27.     }
  28.  
  29.     /**
  30.      * Теперь нужно сообщить о том, какой тип данных используется для хранения адреса
  31.      * @return
  32.      */
  33.     public Class returnedClass() {
  34.         return AddressType.class;
  35.     }
  36.  
  37.     /**
  38.      * Сравнение двух объектов заданного типа данных (после простейших проверок 
  39.      * эту задачу можно делегировать методу equals в составе AddressType)
  40.      * @param x
  41.      * @param y
  42.      * @return
  43.      * @throws HibernateException
  44.      */
  45.     public boolean equals(Object x, Object y) throws HibernateException {
  46.         if (x == null || y == null) return false;
  47.         return x.equals(y);
  48.     }
  49.  
  50.     /**
  51.      * Просто перевызываем метод hashCode для целевого объекта x
  52.      * @param x
  53.      * @return
  54.      * @throws HibernateException
  55.      */
  56.     public int hashCode(Object x) throws HibernateException {
  57.         if (x == null) return 0;
  58.         return x.hashCode();
  59.     }
  60.  
  61.     public Object nullSafeGet(ResultSet rs, String[] names, Object owner) 
  62. throws HibernateException, SQLException {
  63.         AddressType at = new AddressType();
  64.         at.setCountry(rs.getString(names[0]));
  65.         at.setCity(rs.getString(names[1]));
  66.         at.setHome(rs.getString(names[2]));
  67.         at.setPhone(rs.getString(names[3]));
  68.         return at;
  69.     }
  70.  
  71.     public void nullSafeSet(PreparedStatement st, Object value, int index) 
  72. throws HibernateException, SQLException {
  73.         AddressType at = (AddressType) value;
  74.         if (at != null){
  75.             st.setString(index+0, at.getCountry());
  76.             st.setString(index+1, at.getCity());
  77.             st.setString(index+2, at.getHome());
  78.             st.setString(index+3, at.getPhone());
  79.         }
  80.         else{
  81.             st.setNull(index+0, sqlTypes[0]);
  82.             st.setNull(index+1, sqlTypes[1]);
  83.             st.setNull(index+2, sqlTypes[2]);
  84.             st.setNull(index+3, sqlTypes[3]);
  85.         }
  86.     }
  87.  
  88.     /**
  89.      * Здесь нужно создать копию объекта (полную, или глубокую копию)
  90.      * @param value
  91.      * @return
  92.      * @throws HibernateException
  93.      */
  94.     public Object deepCopy(Object value) throws HibernateException {
  95.         if (value == null) return null;
  96.         AddressType at = (AddressType) value;
  97.         return new AddressType (at.getCountry(), at.getCity(), at.getHome(), at.getPhone()); 
  98.     }
  99.  
  100.     /**
  101.      * Да, мы обладаем способностью к изменению
  102.      * @return
  103.      */
  104.     public boolean isMutable() {
  105.         return true;
  106.     }
  107.  
  108.  
  109.     /**
  110.      * Этот метод используется когда нужно объект поместить внутрь cache второго уровня
  111.      * @param value
  112.      * @return
  113.      * @throws HibernateException
  114.      */
  115.     public Serializable disassemble(Object value) throws HibernateException {
  116.         return (Serializable) value;
  117.     }
  118.  
  119.     /**
  120.      * А здесь обратный процесс, когда объект восстанавливается из сериализованного представления себя
  121.      * @param cached
  122.      * @param owner
  123.      * @return
  124.      * @throws HibernateException
  125.      */
  126.     public Object assemble(Serializable cached, Object owner) throws HibernateException {
  127.         return cached;
  128.     }
  129.  
  130.     /**
  131.      * Метод вызывается когда необходимо выполнить свлияние двух объектов
  132.      * @param original
  133.      * @param target
  134.      * @param owner
  135.      * @return
  136.      * @throws HibernateException
  137.      */
  138.     public Object replace(Object original, Object target, Object owner) throws HibernateException {
  139.         return deepCopy(original);
  140.     }
  141. }
Теперь разберем вкратце методы образующие класс:
  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:
  1. @Entity
  2. public class User {
  3.  
  4.     @Id
  5.     @GeneratedValue
  6.     protected Integer id;
  7.  
  8.     protected String fio;
  9.  
  10.     protected AddressType homeAddress;
  11.  
  12.     public User() {
  13.     }
  14.  
  15.     public User(String fio, AddressType homeAddress) {
  16.         this.fio = fio;
  17.         this.homeAddress = homeAddress;
  18.     }
  19.  
  20.     public Integer getId() {
  21.         return id;
  22.     }
  23.  
  24.     public void setId(Integer id) {
  25.         this.id = id;
  26.     }
  27.  
  28.     public String getFio() {
  29.         return fio;
  30.     }
  31.  
  32.     public void setFio(String fio) {
  33.         this.fio = fio;
  34.     }
  35.  
  36.     public AddressType getHomeAddress() {
  37.         return homeAddress;
  38.     }
  39.  
  40.     public void setHomeAddress(AddressType homeAddress) {
  41.         this.homeAddress = homeAddress;
  42.     }
  43. }
Теперь необходимо выполнить правки в xml-файле маппинга для класса User: нам нужно также как и при работе с компонентами указать какие поля в таблице БД будут образовывать наш тип данных.
  1. <hibernate-mapping package="test.db2.model">
  2.  
  3.     <class name="User">
  4.         <id name="id" type="int">
  5.             <generator class="native" />
  6.         </id>
  7.         <property name="fio" type="string" />
  8.         <property name="homeAddress" type="test.db2.model.AddressTypeDescriptor" >
  9.             <column name="country" />
  10.             <column name="city" />
  11.             <column name="home" />
  12.             <column name="phone" />
  13.         </property>
  14.     </class>
  15.  
  16. </hibernate-mapping>
Никаких секретов при работе с свежесозданным типом данных нет. Так что сразу перейду к рассмотрению того как можно решить задачу настройки маппинга, но через аннотации. Для работы я использую аннотацию @Type, обратите внимание на то, что это специфическая для hibernate аннотация. Важно указать не имя класса типа данных AddressType, а имя класса дескриптора для этого класса AddressTypeDescriptor. Кроме того с помощью аннотации @Columns (опять это аннотация специфична, именно, для hibernate) вы можете указать имена полей в таблице БД, на которую выполняется отображение данного типа данных:
  1. @org.hibernate.annotations.Type(type = "test.db2.model.AddressTypeDescriptor")
  2.     @Columns(columns = {
  3.         @Column(name = "home_country"),
  4.         @Column(name = "home_city"),
  5.         @Column(name = "home_home"),
  6.         @Column(name = "home_phone")
  7.    }
  8. )
  9. protected AddressType homeAddress;
Что касается возможности хранения внутри пользовательского типа данных, сложных структур данных, например, коллекций, ассоциаций с другими сущностями, то этого сделать нельзя.

Показанный выше пример создал класс дескриптора нового типа данных как реализующий интерфейс UserType. Теперь попробуем расширить класс AddressTypeDescriptor и заставим его реализовать интерфейс CompositeUserType. Если посмотреть на то, какие методы образуют данный интерфейс, то можете увидеть что многие методы перекочевали из UserType, часть методов при этом немного изменила сигнатуры, чтобы соответствовать новым требованиям. Самый главный вопрос: зачем нужен CompositeUserType и чем он отличается от UserType? Все дело в том, что когда мы создаем новый тип данных, то фактически приводим к созданию новых свойств не однозначно связанных с полями исходной таблицы. Это не составило бы больших проблем, если бы мы ограничились простыми действиями, вроде "создать запись, сохранить, удалить". Однако, когда мы говорим о поиске данных, то возникает проблема: я хочу сформирулировать запрос "найти всех пользователей, которые живут в РБ". Для нас очевидно, что так сконструировать запрос, чтобы он сравнивал значение поля "home_country" со значением "РБ". Однако, чтобы сделать так hibernate должен знать об внутреннем устройстве пользовательского типа данных немного больше, чем то, какие типы полей его образуют, давайте дадим ему это:
  1. package test.db2.model;
  2.  
  3. import org.hibernate.usertype.UserType;
  4. import org.hibernate.usertype.CompositeUserType;
  5. import org.hibernate.HibernateException;
  6. import org.hibernate.Hibernate;
  7. import org.hibernate.engine.SessionImplementor;
  8. import org.hibernate.type.Type;
  9.  
  10. import java.sql.ResultSet;
  11. import java.sql.SQLException;
  12. import java.sql.PreparedStatement;
  13. import java.io.Serializable;
  14.  
  15. public class AddressTypeDescriptor implements CompositeUserType {
  16.  
  17.     /**
  18.      * возвращаем информацию об том какие имена полей образуют новый тип данных
  19.      *
  20.      * @return
  21.      */
  22.     public String[] getPropertyNames() {
  23.         return new String[]{"country", "city", "home", "phone"};
  24.     }
  25.  
  26.     /**
  27.      * возвращаем информацию о том, какие типы полей образуют эти свойства
  28.      *
  29.      * @return
  30.      */
  31.     public Type[] getPropertyTypes() {
  32.         return new Type[]{
  33.                 Hibernate.STRING, Hibernate.STRING,
  34.                 Hibernate.STRING, Hibernate.STRING
  35.         };
  36.     }
  37.  
  38.     /**
  39.      * теперь надо на основании порядкового номера свойства вернуть его значение
  40.      *
  41.      * @param component
  42.      * @param property
  43.      * @return
  44.      * @throws HibernateException
  45.      */
  46.     public Object getPropertyValue(Object component, int property) throws HibernateException {
  47.         AddressType at = (AddressType) component;
  48.         switch (property) {
  49.             case 0:
  50.                 return at.getCountry();
  51.             case 1:
  52.                 return at.getCity();
  53.             case 2:
  54.                 return at.getHome();
  55.             case 3:
  56.                 return at.getPhone();
  57.             default:
  58.                 throw new IllegalArgumentException("invalid property numer '" + property + "'");
  59.         }
  60.     }
  61.  
  62.     /**
  63.      * теперь выполняем парную операцию, устанавливая новое значение для сложного свойства
  64.      *
  65.      * @param component
  66.      * @param property
  67.      * @param value
  68.      * @throws HibernateException
  69.      */
  70.     public void setPropertyValue(Object component, int property, Object value) throws HibernateException {
  71.         AddressType at = (AddressType) component;
  72.         switch (property) {
  73.             case 0:
  74.                 at.setCountry((String) value);
  75.                 break;
  76.             case 1:
  77.                 at.setCity((String) value);
  78.                 break;
  79.             case 2:
  80.                 at.setHome((String) value);
  81.                 break;
  82.             case 3:
  83.                 at.setPhone((String) value);
  84.                 break;
  85.             default:
  86.                 throw new IllegalArgumentException("invalid property numer '" + property + "'");
  87.         }
  88.     }
  89.  
  90.     /**
  91.      * указываем сведения об том типе данных, который обслуживает данный класс
  92.      *
  93.      * @return
  94.      */
  95.     public Class returnedClass() {
  96.         return AddressType.class;
  97.     }
  98.  
  99.  
  100.     /**
  101.      * Сравнение двух объектов заданного типа данных (после простейших проверок
  102.      * эту задачу можно делегировать методу equals в составе AddressType)
  103.      *
  104.      * @param x
  105.      * @param y
  106.      * @return
  107.      * @throws HibernateException
  108.      */
  109.     public boolean equals(Object x, Object y) throws HibernateException {
  110.         if (x == null || y == null) return false;
  111.         return x.equals(y);
  112.     }
  113.  
  114.  
  115.     /**
  116.      * Просто перевызываем метод hashCode для целевого объекта x
  117.      *
  118.      * @param x
  119.      * @return
  120.      * @throws HibernateException
  121.      */
  122.     public int hashCode(Object x) throws HibernateException {
  123.         if (x == null) return 0;
  124.         return x.hashCode();
  125.     }
  126.  
  127.     /**
  128.      * нужно выполнить чтение данных образующих свойство из ResultSet-а
  129.      *
  130.      * @param rs
  131.      * @param names
  132.      * @param session
  133.      * @param owner
  134.      * @return
  135.      * @throws HibernateException
  136.      * @throws SQLException
  137.      */
  138.     public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) 
  139. throws HibernateException, SQLException {
  140.         AddressType at = new AddressType();
  141.         at.setCountry(rs.getString(names[0]));
  142.         at.setCity(rs.getString(names[1]));
  143.         at.setHome(rs.getString(names[2]));
  144.         at.setPhone(rs.getString(names[3]));
  145.         return at;
  146.     }
  147.  
  148.     public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) 
  149. throws HibernateException, SQLException {
  150.         AddressType at = (AddressType) value;
  151.         if (at != null) {
  152.             st.setString(index + 0, at.getCountry());
  153.             st.setString(index + 1, at.getCity());
  154.             st.setString(index + 2, at.getHome());
  155.             st.setString(index + 3, at.getPhone());
  156.         } else {
  157.             st.setNull(index + 0, Hibernate.STRING.sqlType());
  158.             st.setNull(index + 1, Hibernate.STRING.sqlType());
  159.             st.setNull(index + 2, Hibernate.STRING.sqlType());
  160.             st.setNull(index + 3, Hibernate.STRING.sqlType());
  161.         }
  162.     }
  163.  
  164.     /**
  165.      * Здесь нужно создать копию объекта (полную, или глубокую копию)
  166.      * @param value
  167.      * @return
  168.      * @throws HibernateException
  169.      */
  170.     public Object deepCopy(Object value) throws HibernateException {
  171.         if (value == null) return null;
  172.         AddressType at = (AddressType) value;
  173.         return new AddressType (at.getCountry(), at.getCity(), at.getHome(), at.getPhone());
  174.     }
  175.  
  176.  
  177.     /**
  178.      * Да, мы обладаем способностью к изменению
  179.      * @return
  180.      */
  181.     public boolean isMutable() {
  182.         return true;
  183.     }
  184.  
  185.  
  186.     /**
  187.      * Этот метод используется когда нужно объект поместить внутрь cache второго уровня
  188.      * @param value
  189.      * @param session
  190.      * @return
  191.      * @throws HibernateException
  192.      */
  193.     public Serializable disassemble(Object value, SessionImplementor session) throws HibernateException {
  194.         return (Serializable)value;
  195.     }
  196.  
  197.     /**
  198.      * А здесь обратный процесс, когда объект восстанавливается из сериализованного представления себя
  199.      * @param cached
  200.      * @param session
  201.      * @param owner
  202.      * @return
  203.      * @throws HibernateException
  204.      */
  205.     public Object assemble(Serializable cached, SessionImplementor session, Object owner) throws HibernateException {
  206.         return cached;
  207.     }
  208.  
  209.     /**
  210.      * Метод вызывается когда необходимо выполнить свлияние двух объектов
  211.      * @param original
  212.      * @param target
  213.      * @param session
  214.      * @param owner
  215.      * @return
  216.      * @throws HibernateException
  217.      */
  218.     public Object replace(Object original, Object target, SessionImplementor session, Object owner) throws HibernateException {
  219.         return deepCopy(original);
  220.     }
  221. }
Изменений не много: прежде всего добавлены методы, которые позволяют hibernate узнать об логических именах полей образующих пользовательский тип данных (именно, логических). Также появилась пара методов getPropertyValue и setPropertyValue, служащих для изменения значения отдельного значения свойства. Будьте внимательны и во всех методах контролируйте правильный порядок полей свойства.

Теперь попробуем поиграться с новым типом данных для создания hql-запроса (равно, как и criteria-основанного запроса) для поиска данных в БД. Для этого мне пришлось внести очередные правки в класс User (также помимо добавления поля homeAddress хранящего сведения об домашнем адресе пользователя в виде нового типа данных), я вернул в состав User-а информацию об его рабочем адресе (но только в форме рассмотренного в прошлой статье component-а):
  1. package test.db2.model;
  2.  
  3. import org.hibernate.annotations.AccessType;
  4. import org.hibernate.annotations.Columns;
  5.  
  6. import javax.persistence.*;
  7. import java.io.Serializable;
  8.  
  9.  
  10. @Entity
  11. public class User {
  12.  
  13.     @Id
  14.     @GeneratedValue
  15.     protected Integer id;
  16.  
  17.     protected String fio;
  18.  
  19.     @org.hibernate.annotations.Type(type = "test.db2.model.AddressTypeDescriptor")
  20.     @Columns(columns = {
  21.         @Column(name = "home_country"),
  22.         @Column(name = "home_city"),
  23.         @Column(name = "home_home"),
  24.         @Column(name = "home_phone")
  25.     })
  26.     protected AddressType homeAddress;
  27.  
  28.     @AttributeOverrides({
  29.         @AttributeOverride(name = "country.name", column = @Column(name="work_country_name")),
  30.         @AttributeOverride(name = "country.anthem", column = @Column(name="work_country_anthem")),
  31.         @AttributeOverride(name = "city", column = @Column(name="work_city")),
  32.         @AttributeOverride(name = "home", column = @Column(name="work_home")),
  33.         @AttributeOverride(name = "phone", column = @Column(name="work_phone"))
  34.     })
  35.     @Embedded
  36.     protected AddressComponent workAddress;
  37.  
  38.     @ManyToOne (cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
  39.     @JoinColumn (name = "fk_department_id")
  40.     protected Department department;
  41.  
  42.     public User() {
  43.     }
  44.  
  45.     public User(String fio, AddressType homeAddress) {
  46.         this.fio = fio;
  47.         this.homeAddress = homeAddress;
  48.     }
  49.  
  50.     public Integer getId() {
  51.         return id;
  52.     }
  53.  
  54.     public void setId(Integer id) {
  55.         this.id = id;
  56.     }
  57.  
  58.     public String getFio() {
  59.         return fio;
  60.     }
  61.  
  62.     public void setFio(String fio) {
  63.         this.fio = fio;
  64.     }
  65.  
  66.     public AddressType getHomeAddress() {
  67.         return homeAddress;
  68.     }
  69.  
  70.     public void setHomeAddress(AddressType homeAddress) {
  71.         this.homeAddress = homeAddress;
  72.     }
  73.  
  74.     public AddressComponent getWorkAddress() {
  75.         return workAddress;
  76.     }
  77.  
  78.     public void setWorkAddress(AddressComponent workAddress) {
  79.         this.workAddress = workAddress;
  80.     }
  81.  
  82.     public Department getDepartment() {
  83.         return department;
  84.     }
  85.  
  86.     public void setDepartment(Department department) {
  87.         this.department = department;
  88.     }
  89. }
Теперь создадим несколько записей в БД:
  1. Session session = getSession();
  2.  
  3. session.beginTransaction();
  4.  
  5.  
  6. User vasyano = new User("Vasyano Tapkin", new AddressType("Республика Беларусь", "минск", "13", "1234567890"));
  7. User petyano = new User("Petyano Gromov", new AddressType("belarus", null, null, null));
  8. User lenka = new User("Lenka Umkina", null);
  9.  
  10. lenka.setWorkAddress(new AddressComponent(new Country("belarus", "bla-bla-bla"), "minsk", "13", "1234567890"));
  11.  
  12. session.saveOrUpdate(vasyano);
  13. session.saveOrUpdate(petyano);
  14. session.saveOrUpdate(lenka);
  15.  
  16. session.getTransaction().commit();
Теперь попробуем искать данные в БД. Сначала используем запрос на hql, который ищет тех людей, которые проживают (т.е. говорим об домашнем адресе) в Республике Беларусь (belarus):
  1. Query hql_query = session.createQuery("from User where homeAddress.country = :country");
  2. hql_query.setString("country", "belarus");
  3. List hql_list_in_rb = hql_query.list();
Как видите, никаких проблем: я могу обращаться к сложному и логическому имени (homeAddress.country), ведь hibernate знает о том из каких именно полей состоит созданный мною тип данных AddressType.

Второй пример запроса так же работает с hql и домашним адресом клиента. Однако, в этом случае я хочу выполнить поиск не на основании значения отдельных свойств домашнего адреса, а сравнить весь домашний адрес:
  1. AddressType addr_of_lenka = new AddressType("Республика Беларусь", "минск", "13", "1234567890");
  2. Query hql_query2 = session.createQuery("from User where homeAddress = :address");
  3. hql_query2.setParameter("address", addr_of_lenka, Hibernate.custom(AddressTypeDescriptor.class));
  4. List hql2_list_list_by_spec_addr = hql_query2.list();
Я создал переменную addr_of_lenka с заполненными полями шаблона. Затем при создании hql-запроса я пишу "homeAddress = :address" и последний шаг - при выполнении привязки параметра, нужно указать значение типа данных: Hibernate.custom(AddressTypeDescriptor.class));

Небольшое примечание: при выполнении такого запроса возникают проблемы с mysql т.к. посылаемый на сервер запрос выглядит следующим образом:
  1. 23:49:50,437 DEBUG SQL:401 - /* from User where homeAddress = :address */ 
  2. select user0_.id as id0_, user0_.fk_department_id as fk13_0_, user0_.fio as fio0_, user0_.friendly as friendly0_, 
  3. user0_.home_country as home4_0_, user0_.home_city as home5_0_, user0_.home_home as home6_0_, user0_.home_phone as home7_0_,
  4.  user0_.work_city as work8_0_, user0_.work_country_anthem as work9_0_, user0_.work_country_name as work10_0_, 
  5. user0_.work_home as work11_0_, user0_.work_phone as work12_0_ from User user0_ 
  6.  
  7. where (user0_.home_country, user0_.home_city, user0_.home_home, user0_.home_phone)=(?, ?, ?, ?)
Основное внимание на последнюю строку. Вся проблема в том, что при такой записи (а главное зачем) возникает проблема, если коллация соединения не совпадает с коллацией данных в БД. Так у меня бд была в utf8, а коллация соединения latin_1_swedish. Так что для решения проблемы мне пришлось в строку подключения к БД добавить еще один параметр:

jdbc:mysql://localhost/demoarticles?useUnicode=true&amp;characterSet=UTF-8&amp;connectionCollation=utf8_general_ci

Третий пример запроса повторяет функциональность первого вида запроса, но создан с помощью Criteria-api:
  1. Criteria criteria_addtype = DetachedCriteria.forClass(User.class).add(
  2.      Restrictions.eq("homeAddress.country", "belarus")
  3. ).getExecutableCriteria(session);
  4. List hql3_list_by_criteriapi_addrtype = criteria_addtype.list();
Последний пример показывает то, как вы можете выполнить запрос уже по рабочему адресу клиента (напоминаю, что там использовался не новый тип данных, а компонент):
  1. Query hql_query4 = session.createQuery("from User where workAddress.country.name = :countryName");
  2. hql_query4.setString("countryName", "belarus");
  3. List hql4_list_list_by_spec_addr = hql_query4.list();
Как видите с одной стороны работать с компонентами проще, чем с custom data types. А с другой стороны способа написать с помощью компонента запрос аналогичный примеру 2 нет. Важно это или нет - решать вам. А мы переходим к последней теме сегодняшей статьи - работе с интерфейсом ParametrizedType.

Не секрет, что при работе с hibernate, мы сталкиваемся с проблемой несовпадения модели данных в БД и в java. Классическим примером является работа с логическими типами данных: если в java мы объвляем поля класса как boolean - и это удобно, то попытка создать логическое поле в БД может натолкнуться на ряд проблем.

Прежде всего, не все БД поддерживают логический тип данных и в этом случае его приходится эмулировать с помощью, например, целого числа (если нету bit-а, то сойдет и tinyint), для которого 0 играет роль "false", а 1 - "true". Возможен и вариант, когда роль логического типа играет строка из одного символа char(1), значением которой являются такие величины как 'Y/N' или 'T/F' или что там еще вы придумаете.

Показанный мною пример работы с CustomType позволяет вам создать любой из приведенных выше примеров. Однако, предположим, что вы решили создать универсальный тип данных. Т.е. в основе представления логического типа данных в БД используется строка char(1), а вот конкретные значения играющие роль true & false различаются. И для того чтобы не создавать несколько классов с практически идентичной функциональностью (отличия только в двух буквах), то вы хотели бы иметь возможность создать универсальный (гибкий, шаблонный) тип данных "универсальный_boolean_построенный_на_базе_строки_из_одного_символа". Но при этом при объявлении в составе класса некоторого поля использующего этот "тип_данных_с_длинным_названием" вы можете передать ему два параметра: две конкретные строки используемые для представления значения true & false. И именно для этого используется ParametrizedType.

В состав интерфейса входит только один метод, используемый hibernate-ом для передачи параметров влияющих на работу класса:
  1. public void setParameterValues(Properties parameters) {}
Теперь рассмотрим весь код класса дескриптора:
  1. package test.db2.model;
  2.  
  3. import org.hibernate.usertype.ParameterizedType;
  4. import org.hibernate.usertype.UserType;
  5. import org.hibernate.HibernateException;
  6. import org.hibernate.Hibernate;
  7.  
  8. import java.util.Properties;
  9. import java.sql.ResultSet;
  10. import java.sql.SQLException;
  11. import java.sql.PreparedStatement;
  12. import java.io.Serializable;
  13.  
  14. public class SuperBooleanTypeDescriptor  implements UserType, ParameterizedType{
  15.     /**
  16.      * две переменные хранят значения, играющие роль true|false отметок
  17.      */
  18.     protected String trueMarker;
  19.     protected String falseMarker;
  20.  
  21.     /**
  22.      * принимаем и сохраняем значения параметров
  23.      * @param parameters
  24.      */
  25.     public void setParameterValues(Properties parameters) {
  26.         trueMarker = parameters.getProperty("trueMarker");
  27.         falseMarker = parameters.getProperty("falseMarker");
  28.         if (trueMarker == null || falseMarker == null)
  29.             throw new IllegalArgumentException ("SuperBooleanTypeDescriptor requires 2 parameters: trueMarker & falseMarker");
  30.     }
  31.  
  32.     public int[] sqlTypes() {
  33.         // указывем какой тип данных на уровне БД используется для хранения логического значения
  34.         return new int[]{Hibernate.CHARACTER.sqlType()};
  35.     }
  36.  
  37.     public Class returnedClass() {
  38.         return Boolean.class;
  39.     }
  40.  
  41.     public boolean equals(Object x, Object y) throws HibernateException {
  42.         if (x == null || y == null) return false;
  43.         return x.equals(y);
  44.     }
  45.  
  46.     public int hashCode(Object x) throws HibernateException {
  47.         if (x == null) return 0;
  48.         return x.hashCode();
  49.     }
  50.  
  51.     public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
  52.         String s = rs.getString(names[0]);
  53.         if (trueMarker.equals(s)) return true;
  54.         if (falseMarker.equals(s)) return false;
  55.         return null;
  56.     }
  57.  
  58.     public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
  59.         if (value == null)
  60.             st.setString(index, null);
  61.         else
  62.             st.setString(index, ((Boolean)value)?trueMarker:falseMarker);
  63.     }
  64.  
  65.     public Object deepCopy(Object value) throws HibernateException {
  66.         if (value == null) return null;
  67.         return new Boolean (((Boolean)value));
  68.     }
  69.  
  70.     public boolean isMutable() {
  71.         return true;
  72.     }
  73.  
  74.     public Serializable disassemble(Object value) throws HibernateException {
  75.         return (Serializable)value;
  76.     }
  77.  
  78.     public Object assemble(Serializable cached, Object owner) throws HibernateException {
  79.         return cached;
  80.     }
  81.  
  82.     public Object replace(Object original, Object target, Object owner) throws HibernateException {
  83.         return deepCopy(original);
  84.     }
  85. }
Как видите, ничего сложного здесь нет: приняли параметры, проверили их на предмет корректности, и при выполнении операции чтения/записи поля из БД, эти параметры соответствующим образом используются.

Теперь пример кода, который использует созданный шагом ранее тип данных (для этого в составе класса User я создал новое свойство friendly типа boolean и промаркировал его соответсвующим образом):
  1. @Entity
  2. public class User {
  3.  
  4.     @Id
  5.     @GeneratedValue
  6.     protected Integer id;
  7.  
  8.     @Type(type = "test.db2.model.SuperBooleanTypeDescriptor",
  9.             parameters = {
  10.                 @Parameter(name = "trueMarker", value = "t"),
  11.                 @Parameter(name = "falseMarker", value = "f")
  12.                     }
  13.     )
  14.     protected Boolean friendly;
Возможно, что использовать подобную запись (громоздкую аннотацию) не слишком удобно (особенно, если часто) и имеет смысл создать псевдоним для вашего типа данных. Для этого я в файле package-info.java написал что-то вроде:
  1. @TypeDefs ({
  2.     @TypeDef (
  3.             name = "boolean_t_f" , typeClass = SuperBooleanTypeDescriptor.class,
  4.             parameters = {
  5.                 @Parameter(name = "trueMarker", value = "t"),
  6.                 @Parameter(name = "falseMarker", value = "f")
  7.             }
  8.     )
  9. })
  10. package test.db2.model;
  11.  
  12. import org.hibernate.annotations.TypeDefs;
  13. import org.hibernate.annotations.TypeDef;
  14. import org.hibernate.annotations.Parameter;
Как видите я использовал аннотацию (package-level) @TypeDefs для того, чтобы определить новый тип данных "boolean_t_f". Если вы никогда ранее не работали с package-level аннотациями, то не пугайтесь, да действительно эти аннотации нужно писать до слова package. Если вы все сделали верно, то после компиляции в списке результирующих файлов будет файл package-info.class. А теперь нужно нам вернуться к файлу hibernate.cfg.xml и дописать еще одну строчку указывающую, что нужно загрузить содержимое пакета (т.е. этого специального файла):
  1. <mapping class="test.db2.model.User" />
  2.         // ***** то что надо *****
  3.         <mapping package="test.db2.model"/>
  4.  
  5.         <mapping class="test.db2.model.Furniture" />
  6.         <mapping class="test.db2.model.Department" />
Теперь возвращаемся назад к классу User-а и вносим в него следующие правки:
  1. @Entity
  2. public class User {
  3.  
  4.     @Id
  5.     @GeneratedValue
  6.     protected Integer id;
  7.  
  8.     @Type(type = "boolean_t_f")
  9.     protected Boolean friendly;
  10.  
  11.     protected String fio;
Согласитесь, получается гораздо красивее чем в прошлый раз. Если же вы хотите задать такую же конструкцию в виде xml-файла, то нужно написать что-то вроде:
  1. <hibernate-mapping package="test.db2.model">
  2.  
  3.     <typedef class="test.db2.model.SuperBooleanTypeDescriptor" name="boolean_Y_N">
  4.         <param name="trueMarker">Y</param>
  5.         <param name="falseMarker">N</param>
  6.     </typedef>
  7.  
  8.     <class name="User">
  9.         <id name="id" type="int">
  10.             <generator class="native"/>
  11.         </id>
  12.         <property name="fio" type="string"/>
  13.         <property name="homeAddress" type="test.db2.model.AddressTypeDescriptor">
  14.             <column name="country"/>
  15.             <column name="city"/>
  16.             <column name="home"/>
  17.             <column name="phone"/>
  18.         </property>
  19.  
  20.         <component name="workAddress" class="AddressComponent">
  21.             <parent name="homeOwner"/>
  22.             <component name="country" class="Country">
  23.                 <property name="name" column="work_country_name"/>
  24.                 <property name="anthem" column="work_country_anthem"/>
  25.             </component>
  26.             <property name="city" column="work_city"/>
  27.             <property name="home" column="work_home"/>
  28.             <property name="phone" column="work_phone"/>
  29.         </component>
  30.  
  31.  
  32.         <property name="friendly">
  33.             <type name="test.db2.model.SuperBooleanTypeDescriptor">
  34.                 <param name="trueMarker">Y</param>
  35.                 <param name="falseMarker">N</param>
  36.             </type>
  37.         </property>
  38.  
  39.         <!-- или так -->
  40.         <!--
  41.         <property name="friendly" type="boolean_Y_N" />
  42.         -->
  43.     </class>
  44.  
  45. </hibernate-mapping>
Теперь проверим как работает созданный мною новый boolean тип при выполнении запросов к БД:
  1. session.createQuery("from User where friendly = :f").setBoolean("f", false).list()
Все работает как надо: когда возникала проблема как выполнить преобразование моего false к хранящемуся в БД значению 'F', то сработал метод nullSafeSet внутри класса дескриптора типа и преобразовал Boolean в букву 'F'.

Categories: Java