用Hibernate映射PostgreSQL数组
有任何人通过Hibernate成功地将PostgreSQL中的数值数组映射到java中的数值数组?
SQL:
CREATE TABLE sal_emp (name text, pay_by_quarter integer[]); INSERT INTO sal_emp VALUES ('one', '{1,2,3}'); INSERT INTO sal_emp VALUES ('two', '{4,5,6}'); INSERT INTO sal_emp VALUES ('three', '{2,4,6}');
制图:
<hibernate-mapping> <class name="SalEmp" table="sal_emp"> <id name="name" /> <property name="payByQuarter" column="pay_by_quarter" /> </class> </hibernate-mapping>
类:
public class SalEmp implements Serializable{ private String name; private Integer[] payByQuarter; ...// getters & setters }
查询表时,我收到一个exception。
Hibernate不支持开箱即用的数据库数组(例如映射到java.sql.Array
数组)。
由Hibernate提供的array
和primitive-array
types用于将Java数组映射到后台表 – 它们基本上是一对多/元素集合映射的变体,所以这不是您想要的。
最新的PostgreSQL JDBC驱动程序(8.4.whatever)支持JDBC4的Connection.createArrayOf()
方法,以及ResultSet.getArray()
和PreparedStatement.setArray()方法,所以您可以编写自己的UserType
来提供数组支持。
下面是一个处理Oracle数组的UserType实现,它提供了一个很好的起点,相反,它适合处理java.sql.Array
是相当简单的。
也许这对其他人有用:我发现在我的情况下,它performance不佳,不能用于c3p0。 (只是简要地探讨这些问题,是否可以解决请纠正我!)
hibernate3.6:
import java.io.Serializable; import java.sql.Array; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import org.apache.commons.lang.ArrayUtils; import org.hibernate.HibernateException; import org.hibernate.usertype.UserType; public class IntArrayUserType implements UserType { protected static final int SQLTYPE = java.sql.Types.ARRAY; @Override public Object nullSafeGet(final ResultSet rs, final String[] names, final Object owner) throws HibernateException, SQLException { Array array = rs.getArray(names[0]); Integer[] javaArray = (Integer[]) array.getArray(); return ArrayUtils.toPrimitive(javaArray); } @Override public void nullSafeSet(final PreparedStatement statement, final Object object, final int i) throws HibernateException, SQLException { Connection connection = statement.getConnection(); int[] castObject = (int[]) object; Integer[] integers = ArrayUtils.toObject(castObject); Array array = connection.createArrayOf("integer", integers); statement.setArray(i, array); } @Override public Object assemble(final Serializable cached, final Object owner) throws HibernateException { return cached; } @Override public Object deepCopy(final Object o) throws HibernateException { return o == null ? null : ((int[]) o).clone(); } @Override public Serializable disassemble(final Object o) throws HibernateException { return (Serializable) o; } @Override public boolean equals(final Object x, final Object y) throws HibernateException { return x == null ? y == null : x.equals(y); } @Override public int hashCode(final Object o) throws HibernateException { return o == null ? 0 : o.hashCode(); } @Override public boolean isMutable() { return false; } @Override public Object replace(final Object original, final Object target, final Object owner) throws HibernateException { return original; } @Override public Class<int[]> returnedClass() { return int[].class; } @Override public int[] sqlTypes() { return new int[] { SQLTYPE }; } }
在本文中 ,我解释了如何创build一个通用数组types,您可以简单地适应各种特定types,如String[]
或int[]
。
您不必手动创build所有这些types,只需通过Maven Central使用以下依赖项即可获取它们:
<dependency> <groupId>com.vladmihalcea</groupId> <artifactId>hibernate-types-52</artifactId> <version>${hibernate-types.version}</version> </dependency>
有关更多信息,请查看hibernate-type开源项目 。
假设你在你的数据库中有这个表:
create table event ( id int8 not null, version int4, sensor_names text[], sensor_values integer[], primary key (id) )
你想要这样映射它:
@Entity(name = "Event") @Table(name = "event") @TypeDefs({ @TypeDef( name = "string-array", typeClass = StringArrayType.class ), @TypeDef( name = "int-array", typeClass = IntArrayType.class ) }) public static class Event extends BaseEntity { @Type( type = "string-array" ) @Column( name = "sensor_names", columnDefinition = "text[]" ) private String[] sensorNames; @Type( type = "int-array" ) @Column( name = "sensor_values", columnDefinition = "integer[]" ) private int[] sensorValues; //Getters and setters omitted for brevity }
你需要像这样定义StringArrayType
:
public class StringArrayType extends AbstractSingleColumnStandardBasicType<String[]> implements DynamicParameterizedType { public StringArrayType() { super( ArraySqlTypeDescriptor.INSTANCE, StringArrayTypeDescriptor.INSTANCE ); } public String getName() { return "string-array"; } @Override protected boolean registerUnderJavaType() { return true; } @Override public void setParameterValues(Properties parameters) { ((StringArrayTypeDescriptor) getJavaTypeDescriptor()) .setParameterValues(parameters); } }
你需要像这样定义IntArrayType
:
public class IntArrayType extends AbstractSingleColumnStandardBasicType<int[]> implements DynamicParameterizedType { public IntArrayType() { super( ArraySqlTypeDescriptor.INSTANCE, IntArrayTypeDescriptor.INSTANCE ); } public String getName() { return "int-array"; } @Override protected boolean registerUnderJavaType() { return true; } @Override public void setParameterValues(Properties parameters) { ((IntArrayTypeDescriptor) getJavaTypeDescriptor()) .setParameterValues(parameters); } }
String和Inttypes共享ArraySqlTypeDescriptor
:
public class ArraySqlTypeDescriptor implements SqlTypeDescriptor { public static final ArraySqlTypeDescriptor INSTANCE = new ArraySqlTypeDescriptor(); @Override public int getSqlType() { return Types.ARRAY; } @Override public boolean canBeRemapped() { return true; } @Override public <X> ValueBinder<X> getBinder( JavaTypeDescriptor<X> javaTypeDescriptor) { return new BasicBinder<X>( javaTypeDescriptor, this) { @Override protected void doBind( PreparedStatement st, X value, int index, WrapperOptions options ) throws SQLException { AbstractArrayTypeDescriptor<Object> abstractArrayTypeDescriptor = (AbstractArrayTypeDescriptor<Object>) javaTypeDescriptor; st.setArray( index, st.getConnection().createArrayOf( abstractArrayTypeDescriptor.getSqlArrayType(), abstractArrayTypeDescriptor.unwrap( value, Object[].class, options ) ) ); } @Override protected void doBind( CallableStatement st, X value, String name, WrapperOptions options ) throws SQLException { throw new UnsupportedOperationException( "Binding by name is not supported!" ); } }; } @Override public <X> ValueExtractor<X> getExtractor( final JavaTypeDescriptor<X> javaTypeDescriptor) { return new BasicExtractor<X>(javaTypeDescriptor, this) { @Override protected X doExtract( ResultSet rs, String name, WrapperOptions options ) throws SQLException { return javaTypeDescriptor.wrap( rs.getArray(name), options ); } @Override protected X doExtract( CallableStatement statement, int index, WrapperOptions options ) throws SQLException { return javaTypeDescriptor.wrap( statement.getArray(index), options ); } @Override protected X doExtract( CallableStatement statement, String name, WrapperOptions options ) throws SQLException { return javaTypeDescriptor.wrap( statement.getArray(name), options ); } }; } }
您还需要定义Java描述符。
public class StringArrayTypeDescriptor extends AbstractArrayTypeDescriptor<String[]> { public static final StringArrayTypeDescriptor INSTANCE = new StringArrayTypeDescriptor(); public StringArrayTypeDescriptor() { super( String[].class ); } @Override protected String getSqlArrayType() { return "text"; } } public class IntArrayTypeDescriptor extends AbstractArrayTypeDescriptor<int[]> { public static final IntArrayTypeDescriptor INSTANCE = new IntArrayTypeDescriptor(); public IntArrayTypeDescriptor() { super( int[].class ); } @Override protected String getSqlArrayType() { return "integer"; } }
AbstractArrayTypeDescriptor
基类中包含了大量的Java到JDBCtypes的处理:
public abstract class AbstractArrayTypeDescriptor<T> extends AbstractTypeDescriptor<T> implements DynamicParameterizedType { private Class<T> arrayObjectClass; @Override public void setParameterValues(Properties parameters) { arrayObjectClass = ( (ParameterType) parameters .get( PARAMETER_TYPE ) ) .getReturnedClass(); } public AbstractArrayTypeDescriptor(Class<T> arrayObjectClass) { super( arrayObjectClass, (MutabilityPlan<T>) new MutableMutabilityPlan<Object>() { @Override protected T deepCopyNotNull(Object value) { return ArrayUtil.deepCopy( value ); } } ); this.arrayObjectClass = arrayObjectClass; } @Override public boolean areEqual(Object one, Object another) { if ( one == another ) { return true; } if ( one == null || another == null ) { return false; } return ArrayUtil.isEquals( one, another ); } @Override public String toString(Object value) { return Arrays.deepToString((Object[]) value); } @Override public T fromString(String string) { return ArrayUtil.fromString( string, arrayObjectClass ); } @SuppressWarnings({ "unchecked" }) @Override public <X> X unwrap( T value, Class<X> type, WrapperOptions options ) { return (X) ArrayUtil.wrapArray( value ); } @Override public <X> T wrap( X value, WrapperOptions options ) { if( value instanceof Array ) { Array array = (Array) value; try { return ArrayUtil.unwrapArray( (Object[]) array.getArray(), arrayObjectClass ); } catch (SQLException e) { throw new IllegalArgumentException( e ); } } return (T) value; } protected abstract String getSqlArrayType(); }
AbstractArrayTypeDescriptor
依赖于ArrayUtil来处理Java数组深度复制,打包和解包逻辑。
现在,当你插入一些实体时,
Event nullEvent = new Event(); nullEvent.setId(0L); entityManager.persist(nullEvent); Event event = new Event(); event.setId(1L); event.setSensorNames( new String[] { "Temperature", "Pressure" } ); event.setSensorValues( new int[] { 12, 756 } ); entityManager.persist(event);
Hibernate将生成以下SQL语句:
INSERT INTO event ( version, sensor_names, sensor_values, id ) VALUES ( 0, NULL(ARRAY), NULL(ARRAY), 0 ) INSERT INTO event ( version, sensor_names, sensor_values, id ) VALUES ( 0, {"Temperature","Pressure"}, {"12","756"}, 1 )
这已经过对string数组的testing。 数字数组可能需要对转换器进行一些修改。 这与Spring JPA一起工作。
1)将PostgreSQLTextArray
添加到您的项目
import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import java.util.Map; /** * This is class provides {@link java.sql.Array} interface for PostgreSQL <code>text</code> array. * * @author Valentine Gogichashvili * */ public class PostgreSQLTextArray implements java.sql.Array { private final String[] stringArray; private final String stringValue; /** * Initializing constructor * @param stringArray */ public PostgreSQLTextArray(String[] stringArray) { this.stringArray = stringArray; this.stringValue = stringArrayToPostgreSQLTextArray(this.stringArray); } @Override public String toString() { return stringValue; } private static final String NULL = "NULL"; /** * This static method can be used to convert an string array to string representation of PostgreSQL text array. * @param a source String array * @return string representation of a given text array */ public static String stringArrayToPostgreSQLTextArray(String[] stringArray) { final int arrayLength; if ( stringArray == null ) { return NULL; } else if ( ( arrayLength = stringArray.length ) == 0 ) { return "{}"; } // count the string length and if need to quote int neededBufferLentgh = 2; // count the beginning '{' and the ending '}' brackets boolean[] shouldQuoteArray = new boolean[stringArray.length]; for (int si = 0; si < arrayLength; si++) { // count the comma after the first element if ( si > 0 ) neededBufferLentgh++; boolean shouldQuote; final String s = stringArray[si]; if ( s == null ) { neededBufferLentgh += 4; shouldQuote = false; } else { final int l = s.length(); neededBufferLentgh += l; if ( l == 0 || s.equalsIgnoreCase(NULL) ) { shouldQuote = true; } else { shouldQuote = false; // scan for commas and quotes for (int i = 0; i < l; i++) { final char ch = s.charAt(i); switch(ch) { case '"': case '\\': shouldQuote = true; // we will escape these characters neededBufferLentgh++; break; case ',': case '\'': case '{': case '}': shouldQuote = true; break; default: if ( Character.isWhitespace(ch) ) { shouldQuote = true; } break; } } } // count the quotes if ( shouldQuote ) neededBufferLentgh += 2; } shouldQuoteArray[si] = shouldQuote; } // construct the String final StringBuilder sb = new StringBuilder(neededBufferLentgh); sb.append('{'); for (int si = 0; si < arrayLength; si++) { final String s = stringArray[si]; if ( si > 0 ) sb.append(','); if ( s == null ) { sb.append(NULL); } else { final boolean shouldQuote = shouldQuoteArray[si]; if ( shouldQuote ) sb.append('"'); for (int i = 0, l = s.length(); i < l; i++) { final char ch = s.charAt(i); if ( ch == '"' || ch == '\\' ) sb.append('\\'); sb.append(ch); } if ( shouldQuote ) sb.append('"'); } } sb.append('}'); assert sb.length() == neededBufferLentgh; return sb.toString(); } @Override public Object getArray() throws SQLException { return stringArray == null ? null : Arrays.copyOf(stringArray, stringArray.length); } @Override public Object getArray(Map<String, Class<?>> map) throws SQLException { return getArray(); } @Override public Object getArray(long index, int count) throws SQLException { return stringArray == null ? null : Arrays.copyOfRange(stringArray, (int) index, (int) index + count); } @Override public Object getArray(long index, int count, Map<String, Class<?>> map) throws SQLException { return getArray(index, count); } @Override public int getBaseType() throws SQLException { return java.sql.Types.VARCHAR; } @Override public String getBaseTypeName() throws SQLException { return "text"; } @Override public ResultSet getResultSet() throws SQLException { throw new UnsupportedOperationException(); } @Override public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException { throw new UnsupportedOperationException(); } @Override public ResultSet getResultSet(long index, int count) throws SQLException { throw new UnsupportedOperationException(); } @Override public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map) throws SQLException { throw new UnsupportedOperationException(); } @Override public void free() throws SQLException { } }
2)将ListToArrayConverter
添加到您的代码
import org.postgresql.jdbc4.Jdbc4Array; import javax.persistence.AttributeConverter; import javax.persistence.Converter; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @Converter(autoApply = true) public class ListToArrayConveter implements AttributeConverter<List<String>, Object> { @Override public PostgreSQLTextArray convertToDatabaseColumn(List<String> attribute) { if (attribute == null || attribute.isEmpty()) { return null; } String[] rst = new String[attribute.size()]; return new PostgreSQLTextArray(attribute.toArray(rst)); } @Override public List<String> convertToEntityAttribute(Object dbData) { List<String> rst = new ArrayList<>(); try { String[] elements = (String[]) ((Jdbc4Array) dbData).getArray(); for (String element : elements) { rst.add(element); } } catch (SQLException e) { e.printStackTrace(); } return rst; } }
3)使用它!
@Entity @Table(name = "emails") public class Email { [...] @SuppressWarnings("JpaAttributeTypeInspection") @Column(name = "subject", columnDefinition = "text[]") @Convert(converter = ListToArrayConveter.class) private List<String> subject; [...]
这里是我用来做你以后的int[]
UserType,其中还包括nullSafeGet()
和nullSafeSet()
的空检查:
import org.apache.commons.lang.ArrayUtils; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.usertype.UserType; import java.io.Serializable; import java.sql.Array; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class IntegerArrayUserType implements UserType { protected static final int SQLTYPE = java.sql.Types.ARRAY; @Override public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { Array array = rs.getArray(names[0]); if (array == null) { return null; } Integer[] javaArray = (Integer[]) array.getArray(); return ArrayUtils.toPrimitive(javaArray); } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { Connection connection = st.getConnection(); if (value == null) { st.setNull( index, sqlTypes()[0] ); } else { int[] castObject = (int[]) value; Integer[] integers = ArrayUtils.toObject(castObject); Array array = connection.createArrayOf("integer", integers); st.setArray(index, array); } } @Override public Object assemble(final Serializable cached, final Object owner) throws HibernateException { return cached; } @Override public Object deepCopy(final Object o) throws HibernateException { return o == null ? null : ((int[]) o).clone(); } @Override public Serializable disassemble(final Object o) throws HibernateException { return (Serializable) o; } @Override public boolean equals(final Object x, final Object y) throws HibernateException { return x == null ? y == null : x.equals(y); } @Override public int hashCode(final Object o) throws HibernateException { return o == null ? 0 : o.hashCode(); } @Override public boolean isMutable() { return false; } @Override public Object replace(final Object original, final Object target, final Object owner) throws HibernateException { return original; } @Override public Class<int[]> returnedClass() { return int[].class; } @Override public int[] sqlTypes() { return new int[] { SQLTYPE }; } }
我可以通过这里发布的JPA Converter方法将一个String[]
保存到PostgreSQL 9.4和EclipseLink 2.6.2
这似乎是答案的来源
2016年7月1日的Tk421。
从数据库加载数组也很好。
另外添加到persistence.xml
:
<class> com.ssg.fcp.fcp_e761.therealthing.backend.jpa.convert.ListToArrayConverter </class>
请注意, Jdbc4Array
不再存在于Postgre JDBC驱动程序中,请改用:
org.postgresql.jdbc.PgArray
看到这里: 包自org.postgresql.jdbc4缺less自9.4-1207