Статьи

Пользовательский логический тип пользователя с Hibernate JPA

Стандарт ANSI SQL 1999 ввел тип данных BOOLEAN (хотя, к сожалению, только в качестве дополнительной функции). Но на сегодняшний день это все еще не реализовано большинством основных систем баз данных Как следствие, логические столбцы реализованы различными способами. Например, столбцы CHAR, содержащие «Y» или «N», или использующие столбцы BIT. Впоследствии JPA не может предоставить стандартизированный способ отображения логических полей объекта в столбцах базы данных.

Hibernate предлагает пользовательский тип YesNoType для логических реализаций, использующих столбцы CHAR (1), содержащие символы «Y» или «N». Но для других практик вы должны предоставить собственное решение. К счастью, Hibernate предлагает возможность создавать свои собственные пользовательские типы. В этой записи блога я приведу пример одного такого пользовательского логического типа пользователя.

Недавно я столкнулся с устаревшей голландской схемой базы данных, в которой «Y» (для да) и «N» (для нет) представлены «J» («ja») и «N» («урожденная») соответственно. Это исключено при использовании YesNoType Hibernate. В дополнение к сложности был тот факт, что некоторые из этих столбцов использовали CHAR (1), а другие использовали CHAR (2) с дополненным пробелом — не спрашивайте, почему!

В итоге я написал пользовательский тип пользователя, который позволил мне преобразовать следующее:

Отправная точка

01
02
03
04
05
06
07
08
09
10
11
@Entity
@Table(name = "FOO_BAR")
public class FooBar implements Serializable {
    @Column(name = "FOO_ INDICATOR")
    private String fooIndicator;
  
    @Column(name = "BAR_ INDICATOR", length = 2)
    private String barIndicator;
  
    // …
}

в…

Желаемая ситуация

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Entity
@Table(name = "FOO_BAR")
@TypeDefs({
    @TypeDef(name = JaNeeType.NAME, typeClass = JaNeeType.class)
})
public class FooBar implements Serializable {
    @Column(name = "FOO_INDICATOR)
    @Type(type = JaNeeType.NAME)
    private Boolean fooIndicator;
  
    @Column(name = "BAR_INDICATOR", length = 2)
    @Type(type = JaNeeType.NAME, parameters = { @Parameter(name = "length", value = "2") })
    @Type(type = JaNeeType.NAME)
    private Boolean barIndicator;
  
    // …
}

Кодирование пользовательского типа оказалось довольно простым. Мне просто нужно было реализовать интерфейс org.hibernate.usertype.UserType. Работа с переменной длиной столбца включала добавление параметра ‘length’, необходимого для реализации второго интерфейса — org.hibernate.usertype.ParameterizedType.

Ниже приведено то, что я сделал в итоге.

JaNeeType

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package it.jdev.examples.persistence.hibernate;
  
import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Properties;
  
import org.apache.commons.lang3.StringUtils;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
  
/**
 * A type that maps between {@link java.sql.Types#VARCHAR CHAR(1) or CHAR(2)} and {@link Boolean} (using "J " and "N ").
 * <p>
 * Optionally, a parameter "length" can be set that will result in right-padding with spaces up to the
 * specified length.
 */
public class JaNeeType implements UserType, ParameterizedType {
  
    public static final String NAME = "ja_nee";
  
    private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  
    private int length = 1;
  
    @Override
    public int[] sqlTypes() {
        return new int[] { Types.VARCHAR };
    }
  
    @SuppressWarnings("rawtypes")
    @Override
    public Class returnedClass() {
        return Boolean.class;
    }
  
    @Override
    public boolean equals(final Object x, final Object y) throws HibernateException {
        if (x == null || y == null) {
            return false;
        } else {
            return x.equals(y);
        }
    }
  
    @Override
    public int hashCode(final Object x) throws HibernateException {
        assert (x != null);
        return x.hashCode();
    }
  
    @Override
    public Object nullSafeGet(final ResultSet rs, final String[] names, final SessionImplementor session, final Object owner) throws HibernateException, SQLException {
        final String s = rs.getString(names[0]);
        if (StringUtils.isBlank(s)) {
            return false;
        }
        if ("J".equalsIgnoreCase(s.trim())) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
  
    @Override
    public void nullSafeSet(final PreparedStatement st, final Object value, final int index, final SessionImplementor session) throws HibernateException, SQLException {
        String s = Boolean.TRUE.equals(value) ? "J" : "N";
        if (this.length > 1) {
            s = StringUtils.rightPad(s, this.length);
        }
        st.setString(index, s);
    }
  
    @Override
    public Object deepCopy(final Object value) throws HibernateException {
        return value;
    }
  
    @Override
    public boolean isMutable() {
        return true;
    }
  
    @Override
    public Serializable disassemble(final Object value) throws HibernateException {
        return (Serializable) value;
    }
  
    @Override
    public Object assemble(final Serializable cached, final Object owner) throws HibernateException {
        return cached;
    }
  
    @Override
    public Object replace(final Object original, final Object target, final Object owner) throws HibernateException {
        return original;
    }
  
    @Override
    public void setParameterValues(final Properties parameters) {
        if (parameters != null && !parameters.isEmpty()) {
            final String lengthString = parameters.getProperty("length");
            try {
                if (StringUtils.isNotBlank(lengthString)) {
                    this.length = Integer.parseInt(lengthString);
                }
            } catch (final NumberFormatException e) {
                LOGGER.error("Error parsing int " + lengthString, e);
            }
        }
    }
  
}