Як зберегти властивість типу List <String> у JPA?


158

Який найрозумніший спосіб отримати сутність із полем типу «Список»?

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у вашому рядку не може виникнути жодних подій.
розчавити

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

Виправте свій код. Я вважаю це «бібліотечним кодом», тому він повинен бути захисним, наприклад, принаймні, він повинен мати нульові перевірки
ZZ 5

30

Цю відповідь було виконано перед реалізацією JPA2, якщо ви використовуєте JPA2, див. Відповідь ElementCollection вище:

Списки об'єктів всередині модельного об'єкта, як правило, вважаються "OneToMany" зв'язками з іншим об'єктом. Однак, String - сам по собі не є дозволеним клієнтом відносин "Один до багатьох", оскільки у нього немає ідентифікатора.

Отже, ви повинні перетворити свій список рядків у список об'єктів JPA класу аргументу, що містять ідентифікатор та рядок. Ви потенційно можете використовувати рядок як ідентифікатор, що дозволить заощадити трохи місця у вашій таблиці як від видалення поля ідентифікатора, так і шляхом консолідації рядків, коли рядки рівні, але ви втратите можливість впорядкувати аргументи в початковому порядку (оскільки ви не зберігали жодної інформації про замовлення).

Можна також перетворити свій список у @Transient і додати до класу інше поле (argStorage), яке є або VARCHAR (), або CLOB. Потім вам потрібно буде додати 3 функції: 2 з них однакові і повинні перетворити ваш список рядків в єдину String (в argStorage), розмежувану таким чином, що ви можете їх легко розділити. Анотувати ці дві функції (які виконують те саме) за допомогою @PrePersist та @PreUpdate. Нарешті, додайте третю функцію, яка розбиває argStorage до списку Strings ще раз та додайте до нього повідомлення @PostLoad. Таким чином, ваш CLOB буде оновлюватися рядками щоразу, коли ви збираєтесь зберігати команду, і буде тримати поле argStorage оновленим, перш ніж зберігати його в БД.

Я все ж пропоную зробити перший випадок. Пізніше це стосується справжніх стосунків.


Перехід від ArrayList <String> до String із значеннями, розділеними комами, працював на мене.
Кріс Дейл

2
Але це змушує вас використовувати (imho) некрасиві висловлювання, коли запитуєте це поле.
віскісієра

Так, як я вже сказав ... зробіть перший варіант, краще. Якщо ви просто не можете змусити себе це зробити, варіант 2 може працювати.
billjamesdev

15

Згідно з програмою Java Persistence with Hibernate

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

Якщо ви використовуєте сплячий режим, ви можете зробити щось на кшталт:

@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.


12

Ми також можемо цим скористатися.

@Column(name="arguments")
@ElementCollection(targetClass=String.class)
private List<String> arguments;

1
ймовірно, плюс @JoinTable.
phil294

9

Під час використання Hibernate реалізації JPA я виявив, що просто декларування типу як ArrayList замість List дозволяє в сплячку зберігати список даних.

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

@Entity
public class Command implements Serializable {

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

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


}

2
Дякую. Ця робота з усіма реалізаціями JPA, Arraylist is 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виняток при запуску сервера. Оскільки сплячий бажає використовувати інтерфейси колекції.
Paramvir Singh Karwal

9

У мене була така ж проблема, тому я вклав можливе рішення, але наприкінці вирішив застосувати своє ";" відокремлений список рядків.

тому я маю

// a ; separated list of arguments
String arguments;

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

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


1
Це цілком справедливо, але врахуйте зростання вашої програми та еволюцію схеми. Колись у (найближчому) майбутньому ви з часом зможете перейти на підхід, заснований на сутності.
віскісієра

Я згоден, це цілком справедливо. Однак я пропоную повністю переглянути логіку, а також реалізацію коду. Якщо String arguments- це список дозволів доступу, то, маючи особливий символ, a separator, може бути вразливим для атак ескалації привілеїв.
Thang Pham

1
Це дійсно погана порада, ваша рядок може містити, ;який порушить ваш додаток.
агілоб

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
Мені подобається це рішення, оскільки це єдине запропоноване рішення, яке дає повний опис, включаючи структури ТАБЛИЦІ, та пояснює, чому нам потрібні різні примітки.
Жульєн Кронегг

6

Гаразд я знаю його трохи пізно. Але для тих сміливих душ, які будуть бачити це з часом.

Як написано в документації :

@Basic: найпростіший тип відображення у стовпці бази даних. Основна анотація може бути застосована до стійкої змінної властивості або екземпляра будь-якого з наступних типів: примітивні типи Java, [...], перерахунки та будь-який інший тип, який реалізує java.io.Serializable.

Важливою частиною є тип, який реалізує Serializable

Отже, найпростішим і найпростішим у використанні рішенням є просто використання ArrayList замість List (або будь-якого серіалізаційного контейнера):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

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

  • якщо серіалізована модель об'єкта зміниться, ви, можливо, не зможете відновити дані

  • для кожного елемента, що зберігається, додається невелика накладні витрати.

Коротко

зберігати прапори чи кілька елементів досить просто, але я б не рекомендував це зберігати дані, які можуть зрости великими.


спробував це, але таблиця sql зробила тип даних мініатюрним. Це не робить вставку та отримання списку рядків дуже незручним? Або jpa автоматично серіалізує та десеріалізує для вас?
Джао

3

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

Ось зразок для збереження списку String

@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

Моє виправлення цієї проблеми полягало в відокремленні первинного ключа від зовнішнього ключа. Якщо ви використовуєте затемнення та внесли зазначені вище зміни, не забудьте оновити провідник бази даних. Потім відтворіть сутності з таблиць.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.