Сопоставление массива PostgreSQL с Hibernate
Кто-нибудь успешно сопоставил числовой массив в PostgreSQL с числовым массивом в java через Hibernate?
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
}
Я получаю исключение при запросе таблицы.
Ответы
Ответ 1
Hibernate не поддерживает массивы базы данных (например, отображенные на java.sql.Array
) из коробки.
array
и primitive-array
типы, предоставляемые Hibernate, предназначены для сопоставления массивов Java в таблице поддержки - они в основном являются разновидностями сопоставлений один-на-один/набор элементов, так что не то, что вы хотите.
Последний драйвер PostgreSQL JDBC (8.4.whatever) поддерживает JDBC4 Connection.createArrayOf()
, а также ResultSet.getArray()
и методы PreparedStatement.setArray(), хотя вы можете написать свой собственный UserType
для поддержки массива.
Здесь - это реализация UserType, относящаяся к массиву Oracle, которая обеспечивает хорошую отправную точку, разумно просто адаптировать ее для обработки java.sql.Array
вместо этого.
Ответ 2
Возможно, это полезно для кого-то еще: я обнаружил, что в моем случае он работает плохо и не может использоваться с c3p0. (Только кратко изучили эти вопросы, можно ли их решить, исправьте меня!)
Спящий режим 3.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 };
}
}
Ответ 3
В в этой статье, я объяснил, как вы можете создать общий тип массива, который вы можете просто адаптируются к различным конкретным типам, например String[]
или int[]
.
Вам не нужно создавать все эти типы вручную, вы можете просто получить их через Maven Central, используя следующую зависимость:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version>
</dependency>
Для получения дополнительной информации ознакомьтесь с проектом с открытым исходным кодом типа hibernate.
Предположим, что у вас есть эта таблица в вашей базе данных:
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 и Int разделяют 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";
}
}
Основная часть обработки типов Java-to-JDBC включена в базовый класс AbstractArrayTypeDescriptor
:
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
)
Ответ 4
Это было протестировано против строковых массивов. Возможно, некоторые модификации в конвертере необходимы для числовых массивов. Это работает с 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;
[...]
Ответ 5
Вот 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 };
}
}
Ответ 6
Мне удалось сохранить String[]
в PostgreSQL 9.4 и EclipseLink 2.6.2 через JPA-конвертер, опубликованный здесь
который, кажется, является источником ответа
Tk421 от 1 июля 2016 года.
Загрузка массива из БД также хорошо работает.
Дополнительно добавлено к persistence.xml
:
<class> com.ssg.fcp.fcp_e761.therealthing.backend.jpa.convert.ListToArrayConverter </class>
Обратите внимание, что Jdbc4Array
больше нет в драйвере Postgre JDBC, вместо этого используйте:
org.postgresql.jdbc.PgArray
Смотрите здесь:
Пакет org.postgresql.jdbc4 отсутствует с 9.4-1207