Как использовать Spring Data/JPA для вставки в столбец типа массива Postgres?

Скажем, у меня есть таблица postgres:

CREATE TABLE sal_emp (
    name            text,
    pay_by_quarter  integer[],
    schedule        text[][]
);

Смогу ли я использовать Spring Data для вставки в столбцы pay_by_quarter или schedule? Если возможно, как это будет выглядеть как репозиторий и сущность? Мне не удалось найти документацию или примеры, посвященные этому, возможно, из-за того, как он пересекается с более распространенным вариантом использования, вставляя в несколько таблиц отношения «один ко многим». Говоря об этом, я полностью намерен использовать тип данных Postgresql array и никаких реляционных таблиц.


person Zombies    schedule 24.08.2016    source источник


Ответы (2)


Вам нужно создать свой собственный тип и реализовать UserType interface. Основываясь на следующем ответе, я написал универсальный UserType для использования во всех массивах, и он работает, но вы должны использовать не примитивные типы данных (Integer, Long, String,...). В противном случае см. приведенное выше обновление с типом Boolean.

public class GenericArrayUserType<T extends Serializable> implements UserType {

    protected static final int[] SQL_TYPES = { Types.ARRAY };
    private  Class<T> typeParameterClass;

    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return this.deepCopy(cached);
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (T) this.deepCopy(value);
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {

        if (x == null) {
            return y == null;
        }
        return x.equals(y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        if (resultSet.getArray(names[0]) == null) {
            return new Integer[0];
        }

        Array array = resultSet.getArray(names[0]);
        @SuppressWarnings("unchecked")
        T javaArray = (T) array.getArray();
        return javaArray;
    }

    @Override
    public void nullSafeSet(PreparedStatement statement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
        Connection connection = statement.getConnection();
        if (value == null) {
            statement.setNull(index, SQL_TYPES[0]);
        } else {
            @SuppressWarnings("unchecked")
            T castObject = (T) value;
            Array array = connection.createArrayOf("integer", (Object[]) castObject);
            statement.setArray(index, array);
        }
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }

    @Override
    public Class<T> returnedClass() {
        return typeParameterClass;
    }

    @Override
    public int[] sqlTypes() {
        return new int[] { Types.ARRAY };
    }


}

Тогда свойства массива будут иметь тот же тип базы данных с той же размерностью:

  • integer[] -> Integer[]
  • text[][]-> String[][]

И в этих особых случаях поместите класс GenericType над свойствами

@Type(type = "packageofclass.GenericArrayUserType")

Тогда ваша сущность будет:

@Entity
@Table(name="sal_emp")
public class SalEmp {

    @Id
    private String name;

    @Column(name="pay_by_quarter")
    @Type(type = "packageofclass.GenericArrayUserType")
    private Integer[] payByQuarter;

    @Column(name="schedule")
    @Type(type = "packageofclass.GenericArrayUserType")
    private String[][] schedule;

    //Getters, Setters, ToString, equals, and so on

}

Если вы не хотите использовать этот общий UserType тип Integer[] и напишите тип String[][]. Вам нужно написать свои собственные типы, в вашем случае это будет следующим образом:

  • целое []

    public class IntArrayUserType implements UserType {
    
    protected static final int[] SQL_TYPES = { Types.ARRAY };
    
    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return this.deepCopy(cached);
    }
    
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }
    
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Integer[]) this.deepCopy(value);
    }
    
    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
    
        if (x == null) {
            return y == null;
        }
        return x.equals(y);
    }
    
    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }
    
    @Override
    public boolean isMutable() {
        return true;
    }
    
    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        if (resultSet.getArray(names[0]) == null) {
            return new Integer[0];
        }
    
        Array array = resultSet.getArray(names[0]);
        Integer[] javaArray = (Integer[]) array.getArray();
        return javaArray;
    }
    
    @Override
    public void nullSafeSet(PreparedStatement statement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
        Connection connection = statement.getConnection();
        if (value == null) {
            statement.setNull(index, SQL_TYPES[0]);
        } else {
            Integer[] castObject = (Integer[]) value;
            Array array = connection.createArrayOf("integer", castObject);
            statement.setArray(index, array);
        }
    }
    
    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
    
    @Override
    public Class<Integer[]> returnedClass() {
        return Integer[].class;
    }
    
    @Override
    public int[] sqlTypes() {
        return new int[] { Types.ARRAY };
    }
    }
    
  • текст[][]

    public class StringMultidimensionalArrayType implements UserType {
    
    protected static final int[] SQL_TYPES = { Types.ARRAY };
    
    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return this.deepCopy(cached);
    }
    
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }
    
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (String[][]) this.deepCopy(value);
    }
    
    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
    
        if (x == null) {
            return y == null;
        }
        return x.equals(y);
    }
    
    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }
    
    @Override
    public boolean isMutable() {
        return true;
    }
    
    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        if (resultSet.getArray(names[0]) == null) {
            return new String[0][];
        }
    
        Array array = resultSet.getArray(names[0]);
        String[][] javaArray = (String[][]) array.getArray();
        return javaArray;
    }
    
    @Override
    public void nullSafeSet(PreparedStatement statement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
        Connection connection = statement.getConnection();
        if (value == null) {
            statement.setNull(index, SQL_TYPES[0]);
        } else {
            String[][] castObject = (String[][]) value;
            Array array = connection.createArrayOf("integer", castObject);
            statement.setArray(index, array);
        }
    }
    
    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
    
    @Override
    public Class<String[][]> returnedClass() {
        return String[][].class;
    }
    
    @Override
    public int[] sqlTypes() {
        return new int[] { Types.ARRAY };
    }
    
    }
    

В этом случае ваши свойства имеют разные типы:

@Column(name="pay_by_quarter")
@Type(type = "packageofclass.IntArrayUserType")
private Integer[] payByQuarter;

@Column(name="schedule")
@Type(type = "packageofclass.StringMultidimensionalArrayType")
private String[][] schedule;

Обновить тип пользователя Hibernate

С логическим или логическим значением кажется, что это не работает с GenericArrayUserType, поэтому решения могут быть созданы в вашем CREATE DDL объявлении booleanтипа bytea:

CREATE TABLE sal_emp (
    name text,
    pay_by_quarter  integer[],
    schedule        text[][],
    wow_boolean     bytea
    );

И ваше свойство без какого-либо типа:

private boolean[][][] wowBoolean;

Он очень хорошо разбирает без всяких Typeили Converter. Выход: wowBoolean=[[[true, false], [true, false]], [[true, true], [true, true]]])

Обновление с @Converter из JPA 2.1

Пробовал вариант с @Converterиз JPA 2.1 с EclipseLinkи Hibernate. Я только что попробовал integer[] (не text[][]) Converterподобно этому (*я изменил свойство на List<Integer>, но это не имеет значения):

@Converter
public class ConverterListInteger implements AttributeConverter<List<Integer>, Array>{

    @Override
    public Array convertToDatabaseColumn(List<Integer> attribute) {
        DataSource source = ApplicationContextHolder.getContext().getBean(DataSource.class);

        try {
            Connection conn = source.getConnection();
            Array array = conn.createArrayOf("integer", attribute.toArray());
            return  array;

        } catch (SQLException e) {
            e.printStackTrace();
        }

        return null;

    }

    @Override
    public List<Integer> convertToEntityAttribute(Array dbData) {
        List<Integer> list = new ArrayList<>();

        try {
            for(Object object : (Object[]) dbData.getArray()){
                list.add((Integer) object);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return list;

    }

}

Затем добавьте преобразователь к свойству в Entity:

@Convert(converter=ConverterListInteger.class)
private List<Integer> pay_by_quarter;

Так что решение на основе JPA specification не работает. Почему? Hibernate не поддерживает массивы баз данных (java.sql.Array)....

Затем я попытался использовать EclipseLink (см., как настроить здесь) и это работает, но не всегда... Кажется, это ошибка. В первый раз это работает хорошо, но в следующий раз невозможно обновить или запросить эту строку. Затем мне удалось добавить новые строки, но после этого невозможно обновить или запросить....

Заключение

На данный момент кажется, что JPA производители не поддерживают должным образом... Хорошо работает только решение с Hibernate UserType, но это только для Hibernate.

person Pau    schedule 26.08.2016
comment
Спасибо тебе за это. Есть ли способ сделать это, не завися от Hibernate?? Например, можно ли не использовать org.hibernate.usertype.UserType ?? Или зависит от Hibernate единственное решение для этого варианта использования? - person Zombies; 26.08.2016
comment
Это решение зависит от hibernate, которое я знаю, но оно кажется проще. Если вы не хотите зависеть от «Hibernate», вы можете использовать @Converter из Jpa 2.1. См. этот пост: stackoverflow.com/a/24194996/4751165 - person Pau; 26.08.2016
comment
В примере, имеющем дело с JPA 2.1, вы используете тип Array, то есть java.sql.Array, верно? - person harm; 09.08.2017
comment
Вы никогда не инициализировали typeParameterClass в своем первом блоке кода. Разве вы не получаете исключение нулевого указателя? - person sat; 15.12.2017
comment
Я получаю следующую ошибку, используя GenericArrayUserType: Нет сопоставления диалектов для типа JDBC: 2003 - person chomp; 23.10.2018
comment
@chomp: в @Column добавьте что-то вроде columnDefinition = "text[]". - person Johannes Flügel; 07.12.2018
comment
Это не сработало для меня. В итоге я использовал эту библиотеку: github.com/vladmihalcea/hibernate-types - person chomp; 07.12.2018
comment
К сожалению, среди прочего метод equals в IntArrayUserType не работает. Если x является int[] и y является Integer[] с одинаковыми числами, x.equals(y) будет ложным. - person Johannes Flügel; 14.12.2018

Простой подход

попробуйте преобразовать строку [] в строку, а затем в классе Entity сделайте

@Column(name = "nice_work" columnDefinition="text")

функция для преобразования строки[] в строку и наоборот

private static String stringArrayTOString(String[] input) {
        StringBuffer sb =new StringBuffer("");
        int i=0;
        for(String value:input) {
            
            if(i!=0) {
                sb.append(",");
            }
            sb.append(value);
            i++;
        }
        return sb.toString();
    }
    
    private static String[] stringToStringArray(String input) {
        String[] output = input.split(",");
        return output;
    }
person abhinav kumar    schedule 04.08.2020