Как сохранить свойство типа List <String> в JPA?


158

Какой самый умный способ получить сущность с полем типа List сохраняется?

Command.java

package persistlistofstring;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Persistence;

@Entity
public class Command implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;
    @Basic
    List<String> arguments = new ArrayList<String>();

    public static void main(String[] args) {
        Command command = new Command();

        EntityManager em = Persistence
                .createEntityManagerFactory("pu")
                .createEntityManager();
        em.getTransaction().begin();
        em.persist(command);
        em.getTransaction().commit();
        em.close();

        System.out.println("Persisted with id=" + command.id);
    }
}

Этот код производит:

> Exception in thread "main" javax.persistence.PersistenceException: No Persistence provider for EntityManager named pu: Provider named oracle.toplink.essentials.PersistenceProvider threw unexpected exception at create EntityManagerFactory: 
> oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Local Exception Stack: 
> Exception [TOPLINK-30005] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Exception Description: An exception was thrown while searching for persistence archives with ClassLoader: sun.misc.Launcher$AppClassLoader@11b86e7
> Internal Exception: javax.persistence.PersistenceException: Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
> Exception Description: predeploy for PersistenceUnit [pu] failed.
> Internal Exception: Exception [TOPLINK-7155] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
> Exception Description: The type [interface java.util.List] for the attribute [arguments] on the entity class [class persistlistofstring.Command] is not a valid type for a serialized mapping. The attribute type must implement the Serializable interface.
>         at oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException.exceptionSearchingForPersistenceResources(PersistenceUnitLoadingException.java:143)
>         at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:169)
>         at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:110)
>         at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
>         at persistlistofstring.Command.main(Command.java:30)
> Caused by: 
> ...

Ответы:


197

Используйте некоторую реализацию JPA 2: она добавляет аннотацию @ElementCollection, похожую на Hibernate, которая делает именно то, что вам нужно. Там один пример здесь .

редактировать

Как указано в комментариях ниже, правильная реализация JPA 2

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

См. Http://docs.oracle.com/javaee/6/api/javax/persistence/ElementCollection.html.


1
Моя ошибка состояла в том, чтобы добавить аннотацию @ OneToMany также ... после ее удаления и просто оставив @ ElementCollection, она работала
Вилли Ментцель

47

Извините за восстановление старого потока, но если кто-то ищет альтернативное решение, где вы храните свои списки строк как одно поле в вашей базе данных, вот как я решил это. Создайте конвертер как это:

import java.util.Arrays;
import java.util.List;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
    private static final String SPLIT_CHAR = ";";

    @Override
    public String convertToDatabaseColumn(List<String> stringList) {
        return String.join(SPLIT_CHAR, stringList);
    }

    @Override
    public List<String> convertToEntityAttribute(String string) {
        return Arrays.asList(string.split(SPLIT_CHAR));
    }
}

Теперь используйте его на ваших сущностях следующим образом:

@Convert(converter = StringListConverter.class)
private List<String> yourList;

В базе данных ваш список будет храниться как foo; bar; foobar, а в вашем Java-объекте вы получите список с этими строками.

Надеюсь, это кому-нибудь пригодится.


Будет ли он работать с репозиториями jpa для фильтрации результатов по содержимому этого поля?
Please_Dont_Bully_Me_SO_Lords

1
@Please_Dont_Bully_Me_SO_Lords Это менее подходит для этого варианта использования, так как ваши данные будут в базе данных как "foo; bar; foobar". Если вы хотите запросить данные, то, вероятно, ElementCollection + JoinTable - это путь для вашей ситуации.
Джонк ван дер Когель

Это также означает, что SPLIT_CHARв вашей строке не должно быть ни одного вхождения.
раздавить

@ влюблен, это правильно. Хотя, конечно, вы могли бы разрешить это, например, кодируя вашу строку после того, как вы правильно ее разграничили. Но решение, которое я разместил здесь, в первую очередь предназначено для простых случаев использования; для более сложных ситуаций, вероятно, вам будет лучше с ElementCollection + JoinTable
Jonck van der Kogel

Пожалуйста, исправьте ваш код. Я считаю его «библиотечным кодом», поэтому он должен быть защитным, например, по крайней мере, он должен иметь нулевые проверки
ZZ 5

30

Этот ответ был сделан до реализации JPA2, если вы используете JPA2, см. Ответ ElementCollection выше:

Списки объектов внутри модельного объекта обычно рассматриваются как отношения «OneToMany» с другим объектом. Однако String (сам по себе) не является допустимым клиентом отношения «один ко многим», поскольку у него нет идентификатора.

Таким образом, вы должны преобразовать свой список строк в список объектов JPA класса Argument, содержащих идентификатор и строку. Потенциально вы можете использовать String в качестве идентификатора, что сэкономит немного места в вашей таблице как от удаления поля ID, так и за счет объединения строк, в которых строки равны, но вы потеряете возможность упорядочить аргументы обратно в исходный порядок. (так как вы не хранили никакой информации для заказа).

Кроме того, вы можете преобразовать свой список в @Transient и добавить другое поле (argStorage) в свой класс, которое будет либо VARCHAR (), либо CLOB. Затем вам нужно будет добавить 3 функции: 2 из них одинаковы и должны преобразовать ваш список строк в одну строку (в argStorage), разделенную таким образом, чтобы вы могли легко разделить их. Аннотируйте эти две функции (каждая из которых делает одно и то же) с помощью @PrePersist и @PreUpdate. Наконец, добавьте третью функцию, которая снова разбивает argStorage в список строк и аннотируйте ее @PostLoad. Это будет поддерживать ваш CLOB обновленным со строками всякий раз, когда вы сохраняете Команду, и обновлять поле argStorage, прежде чем сохранять его в БД.

Я все еще предлагаю сделать первый случай. Это хорошая практика для настоящих отношений позже.


Переход с ArrayList <String> на String со значениями, разделенными запятыми, работал для меня.
Крис Дейл

2
Но это заставляет вас использовать (imho) некрасивые операторы при запросе этого поля.
вискисьерра

Да, как я уже сказал ... сделать первый вариант, это лучше. Если вы просто не можете заставить себя сделать это, вариант 2 может сработать.
billjamesdev

15

По словам Java Постоянство с Hibernate

отображение коллекций типов значений с аннотациями [...]. На момент написания статьи это не было частью стандарта Java Persistence

Если бы вы использовали Hibernate, вы могли бы сделать что-то вроде:

@org.hibernate.annotations.CollectionOfElements(
    targetElement = java.lang.String.class
)
@JoinTable(
    name = "foo",
    joinColumns = @JoinColumn(name = "foo_id")
)
@org.hibernate.annotations.IndexColumn(
    name = "POSITION", base = 1
)
@Column(name = "baz", nullable = false)
private List<String> arguments = new ArrayList<String>();

Обновление: обратите внимание, теперь это доступно в JPA2.



10

У меня была та же проблема, поэтому я вложил возможное решение, но в конце я решил реализовать свой ';' разделенный список строк.

так что я

// a ; separated list of arguments
String arguments;

public List<String> getArguments() {
    return Arrays.asList(arguments.split(";"));
}

Таким образом, список легко читается / редактируется в таблице базы данных;


1
Это полностью верно, но учитывайте рост вашего приложения и эволюцию схемы. Когда-нибудь в (ближайшем) будущем вы можете в конечном итоге перейти на подход, основанный на сущностях.
вискисьерра

Я согласен, это совершенно верно. Тем не менее, я предлагаю полностью пересмотреть логику, а также реализацию кода. Если String argumentsпредставляет собой список разрешений доступа, то наличие специального символа a separatorможет быть уязвимым для атак на повышение привилегий.
Тханг Фам

1
Это действительно плохой совет, ваша строка может содержать, ;что сломает ваше приложение.
agilob

9

При использовании реализации JPA в Hibernate, я обнаружил, что простое объявление типа как ArrayList вместо List позволяет hibernate хранить список данных.

Очевидно, что это имеет ряд недостатков по сравнению с созданием списка объектов Entity. Нет ленивой загрузки, нет возможности ссылаться на объекты в списке из других объектов, возможно, больше сложностей при построении запросов к базе данных. Однако, когда вы имеете дело со списками довольно примитивных типов, которые вы всегда захотите получить вместе с сущностью, тогда этот подход мне подходит.

@Entity
public class Command implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;

    ArrayList<String> arguments = new ArrayList<String>();


}

2
Спасибо. Эта работа со всеми реализациями JPA, Arraylist Serializable сохраняется в поле BLOB. Проблемы с этим методом заключаются в том, что 1) размер BLOB-файла фиксирован, 2) вы можете искать или индексировать элементы массива, 3) только клиент, осведомленный о формате сериализации Java, может прочитать эти элементы.
Андреа Франсия

В случае, если вы попробуете этот подход, @OneToMany @ManyToOne @ElementCollectionон даст вам Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementsисключение при запуске сервера. Потому что hibernates хочет, чтобы вы использовали интерфейсы коллекций.
Парамвир Сингх Карвал

9

Кажется, что ни один из ответов не исследовал наиболее важные настройки для @ElementCollectionсопоставления.

Когда вы сопоставляете список с этой аннотацией и позволяете JPA / Hibernate автоматически генерировать таблицы, столбцы и т. Д., Он также будет использовать автоматически сгенерированные имена.

Итак, давайте проанализируем базовый пример:

@Entity
@Table(name = "sample")
public class MySample {

    @Id
    @GeneratedValue
    private Long id;

    @ElementCollection // 1
    @CollectionTable(name = "my_list", joinColumns = @JoinColumn(name = "id")) // 2
    @Column(name = "list") // 3
    private List<String> list;

}
  1. Основная @ElementCollectionаннотация (где вы можете определить известные fetchи targetClassпредпочтения)
  2. @CollectionTableАннотация очень полезно , когда речь идет , чтобы дать имя таблицы , которая будет сгенерирована, а также определения , как joinColumns, foreignKey«s, indexes, uniqueConstraintsи т.д.
  3. @ColumnВажно определить имя столбца, в котором будет храниться varcharзначение списка.

Созданное создание DDL будет:

-- table sample
CREATE TABLE sample (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);

-- table my_list
CREATE TABLE IF NOT EXISTS my_list (
  id bigint(20) NOT NULL,
  list varchar(255) DEFAULT NULL,
  FOREIGN KEY (id) REFERENCES sample (id)
);

4
Мне нравится это решение, потому что это единственное предлагаемое решение, которое дает полное описание, включая структуры TABLE, и объясняет, почему нам нужны разные аннотации.
Жюльен Кронегг

6

Хорошо, я знаю, что немного поздно. Но для тех смельчаков, которые увидят это с течением времени.

Как написано в документации :

@Basic: самый простой тип отображения в столбце базы данных. Аннотация Basic может быть применена к постоянному свойству или переменной экземпляра любого из следующих типов: примитивные типы Java, [...], перечисления и любой другой тип, который реализует java.io.Serializable.

Важной частью является тип, который реализует Сериализуемый

Таким образом, на сегодняшний день самым простым и простым в использовании решением является просто использование ArrayList вместо List (или любого сериализуемого контейнера):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Однако помните, что при этом будет использоваться сериализация системы, поэтому она будет стоить дорого, например:

  • если изменится сериализованная объектная модель, вы не сможете восстановить данные

  • небольшие накладные расходы добавляются для каждого хранимого элемента.

Коротко

хранить флаги или несколько элементов довольно просто, но я бы не советовал хранить данные, которые могут стать большими.


попробовал это, но таблица sql сделала тип данных крошечным шариком. Разве это не делает вставку и извлечение списка строк очень неудобным? Или jpa автоматически сериализует и десериализует для вас?
Джао

3

Тиаго ответ правильный, добавив образец, более специфичный для вопроса, @ElementCollection создаст новую таблицу в вашей базе данных, но без сопоставления двух таблиц. Это означает, что коллекция - это не коллекция сущностей, а коллекция простых типов (строки и т. Д.). .) или набор встраиваемых элементов (класс, аннотированный @Embeddable ).

Вот пример для сохранения списка строк

@ElementCollection
private Collection<String> options = new ArrayList<String>();

Вот пример для сохранения списка пользовательских объектов

@Embedded
@ElementCollection
private Collection<Car> carList = new ArrayList<Car>();

Для этого случая нам нужно сделать класс Embeddable

@Embeddable
public class Car {
}

3

Вот решение для хранения набора с использованием @Converter и StringTokenizer. Еще немного проверок против решения @ jonck-van-der-kogel .

В вашем классе Entity:

@Convert(converter = StringSetConverter.class)
@Column
private Set<String> washSaleTickers;

StringSetConverter:

package com.model.domain.converters;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

@Converter
public class StringSetConverter implements AttributeConverter<Set<String>, String> {
    private final String GROUP_DELIMITER = "=IWILLNEVERHAPPEN=";

    @Override
    public String convertToDatabaseColumn(Set<String> stringList) {
        if (stringList == null) {
            return new String();
        }
        return String.join(GROUP_DELIMITER, stringList);
    }

    @Override
    public Set<String> convertToEntityAttribute(String string) {
        Set<String> resultingSet = new HashSet<>();
        StringTokenizer st = new StringTokenizer(string, GROUP_DELIMITER);
        while (st.hasMoreTokens())
            resultingSet.add(st.nextToken());
        return resultingSet;
    }
}

1

Мое исправление для этой проблемы состояло в том, чтобы отделить первичный ключ от внешнего ключа. Если вы используете eclipse и внесли вышеуказанные изменения, пожалуйста, не забудьте обновить проводник базы данных. Затем воссоздайте сущности из таблиц.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.