Jak zachować właściwość typu List <String> w JPA?


158

Jaki jest najmądrzejszy sposób na utrwalenie encji z polem typu 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);
    }
}

Ten kod daje:

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

Odpowiedzi:


197

Użyj implementacji JPA 2: dodaje adnotację @ElementCollection, podobną do Hibernate, która robi dokładnie to, czego potrzebujesz. Oto jeden przykład .

Edytować

Jak wspomniano w komentarzach poniżej, prawidłowa implementacja JPA 2 to

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

Zobacz: http://docs.oracle.com/javaee/6/api/javax/persistence/ElementCollection.html


1
Moim błędem było dodanie adnotacji @ OneToMany ... po jej usunięciu i opuszczeniu @ ElementCollection zadziałało
Willi Mentzel

47

Przepraszam, że przywracam stary wątek, ale jeśli ktoś szuka alternatywnego rozwiązania, w którym przechowujesz listy ciągów jako jedno pole w bazie danych, oto jak to rozwiązałem. Utwórz konwerter w następujący sposób:

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));
    }
}

Teraz użyj go na swoich Jednostkach w ten sposób:

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

W bazie danych Twoja lista będzie przechowywana jako foo; bar; foobar, aw obiekcie Java otrzymasz listę z tymi ciągami.

Mam nadzieję, że to komuś pomoże.


Czy będzie działać z repozytoriami jpa do filtrowania wyników według zawartości tego pola?
Please_Dont_Bully_Me_SO_Lords

1
@Please_Dont_Bully_Me_SO_Lords Jest to mniej odpowiednie dla tego przypadku użycia, ponieważ Twoje dane będą w bazie danych jako „foo; bar; foobar”. Jeśli chcesz zapytać o dane, prawdopodobnie najlepszym rozwiązaniem będzie elementCollection + JoinTable.
Jonck van der Kogel

Oznacza to również, że nie możesz mieć żadnych SPLIT_CHARwystąpień w swoim ciągu.
zmiażdżyć

@crush, to prawda. Chociaż oczywiście możesz na to zezwolić, na przykład zakodowując swój ciąg znaków po prawidłowym rozgraniczeniu. Ale rozwiązanie, które tutaj zamieściłem, jest przeznaczone przede wszystkim do prostych przypadków użycia; w bardziej skomplikowanych sytuacjach prawdopodobnie wypadniesz lepiej z ElementCollection + JoinTable
Jonck van der Kogel

Popraw kod. Uważam to za „kod biblioteki”, więc powinien być defensywny, np. Przynajmniej powinien mieć sprawdzanie zerowe
ZZ 5

30

Ta odpowiedź została utworzona przed implementacjami JPA2, jeśli używasz JPA2, zobacz odpowiedź ElementCollection powyżej:

Listy obiektów wewnątrz obiektu modelu są ogólnie uważane za relacje typu „OneToMany” z innym obiektem. Jednak String nie jest (sam w sobie) dopuszczalnym klientem relacji jeden-do-wielu, ponieważ nie ma identyfikatora.

Dlatego należy przekonwertować listę ciągów znaków na listę obiektów JPA klasy argumentów, zawierającą identyfikator i ciąg. Potencjalnie można użyć String jako identyfikatora, co pozwoliłoby zaoszczędzić trochę miejsca w tabeli zarówno przed usunięciem pola ID, jak i konsolidacją wierszy, w których ciągi są równe, ale straciłbyś możliwość uporządkowania argumentów z powrotem w ich pierwotnej kolejności (ponieważ nie zapisałeś żadnych informacji o zamówieniu).

Alternatywnie możesz przekonwertować swoją listę na @Transient i dodać kolejne pole (argStorage) do swojej klasy, które jest albo VARCHAR (), albo CLOB. Będziesz wtedy musiał dodać 3 funkcje: 2 z nich są takie same i powinny przekształcić twoją listę ciągów w pojedynczy ciąg (w argStorage) rozdzielony w sposób umożliwiający ich łatwe rozdzielenie. Dodaj adnotacje do tych dwóch funkcji (każda z nich robi to samo) za pomocą @PrePersist i @PreUpdate. Na koniec dodaj trzecią funkcję, która ponownie dzieli argStorage na listę ciągów znaków i dodaj do niej adnotację @PostLoad. Dzięki temu CLOB będzie aktualizowany za pomocą ciągów znaków za każdym razem, gdy przechodzisz do przechowywania polecenia, a pole argStorage będzie aktualizowane przed zapisaniem go w DB.

Nadal sugeruję zrobienie pierwszego przypadku. To dobra praktyka dla prawdziwych relacji później.


Zmiana z ArrayList <String> na String z wartościami oddzielonymi przecinkami zadziałała dla mnie.
Chris Dale,

2
Ale to zmusza cię do używania (imho) brzydkich instrukcji podczas sprawdzania tego pola.
whiskeysierra

Tak, jak powiedziałem ... zrób pierwszą opcję, tak jest lepiej. Jeśli po prostu nie możesz się do tego zmusić, opcja 2 może zadziałać.
billjamesdev

15

Według Java Persistence with Hibernate

mapowanie kolekcji typów wartości z adnotacjami [...]. W chwili pisania tego tekstu nie jest częścią standardu Java Persistence

Jeśli używałeś Hibernacji, możesz zrobić coś takiego:

@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>();

Aktualizacja: Uwaga, jest to teraz dostępne w JPA2.


12

Możemy również tego użyć.

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

1
prawdopodobnie plus @JoinTable.
phil294

9

Korzystając z implementacji JPA Hibernate, odkryłem, że po prostu zadeklarowanie typu jako ArrayList zamiast List umożliwia hibernację do przechowywania listy danych.

Oczywiście ma to wiele wad w porównaniu do tworzenia listy obiektów Entity. Brak leniwego ładowania, brak możliwości odwoływania się do jednostek na liście z innych obiektów, być może większe trudności w tworzeniu zapytań do bazy danych. Jeśli jednak masz do czynienia z listami dość prymitywnych typów, które zawsze będziesz chciał chętnie pobrać wraz z bytem, ​​to takie podejście wydaje mi się dobre.

@Entity
public class Command implements Serializable {

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

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


}

2
Dzięki. Ta praca ze wszystkimi implementacjami JPA, Arraylist jest serializowalna, jest zapisywana w polu BLOB. Problem z tą metodą polega na tym, że 1) rozmiar BLOB jest stały 2) można przeszukiwać lub indeksować elementy tablicy 3) tylko klient znający format serializacji Java może odczytać te elementy.
Andrea Francia,

W przypadku, gdy spróbujesz tego podejścia @OneToMany @ManyToOne @ElementCollection, da ci to Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementswyjątek podczas uruchamiania serwera. Ponieważ hibernacja chce, abyś używał interfejsów kolekcji.
Paramvir Singh Karwal

9

Miałem ten sam problem, więc zainwestowałem w możliwe rozwiązanie, ale ostatecznie zdecydowałem się wdrożyć moje ';' oddzielona lista ciągów.

więc mam

// a ; separated list of arguments
String arguments;

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

W ten sposób lista jest łatwa do odczytania / edycji w tabeli bazy danych;


1
Jest to całkowicie uzasadnione, ale weź pod uwagę rozwój aplikacji i ewolucję schematu. Kiedyś w (bliskiej) przyszłości możesz ostatecznie przejść na podejście oparte na encji.
whiskeysierra

Zgadzam się, to jest całkowicie ważne. Proponuję jednak w pełni przejrzeć logikę, a także implementację kodu. Jeśli String argumentsjest listą uprawnień dostępu, to posiadanie znaku specjalnego a separator, może być podatne na ataki eskalacji uprawnień.
Thang Pham

1
To naprawdę zła rada, Twój ciąg może zawierać informacje, ;które spowodują uszkodzenie aplikacji.
agilob

9

Wydaje się, że żadna z odpowiedzi nie dotyczyła najważniejszych ustawień @ElementCollectionmapowania.

Kiedy mapujesz listę z tą adnotacją i pozwalasz JPA / Hibernate automatycznie generować tabele, kolumny itp., Użyje również automatycznie wygenerowanych nazw.

Przeanalizujmy więc podstawowy przykład:

@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. Podstawowa @ElementCollectionadnotacja (gdzie możesz zdefiniować znane fetchi targetClasspreferencje)
  2. @CollectionTableAdnotacja jest bardzo przydatna, jeśli chodzi o nadanie nazwy tabeli to będzie generowany, a także definicje, takie jak joinColumns, foreignKey„s, indexes, uniqueConstraints, itd.
  3. @Columnważne jest, aby zdefiniować nazwę kolumny, w której będzie przechowywana varcharwartość listy.

Wygenerowane tworzenie DDL wyglądałoby tak:

-- 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
Podoba mi się to rozwiązanie, ponieważ jest to jedyne proponowane rozwiązanie, które daje pełny opis, w tym struktury TABELI i wyjaśnia, dlaczego potrzebujemy różnych adnotacji.
Julien Kronegg

6

Ok, wiem, że jest trochę za późno. Ale dla tych odważnych dusz, które zobaczą to w miarę upływu czasu.

Jak napisano w dokumentacji :

@Basic: Najprostszy typ mapowania do kolumny bazy danych. Adnotację Basic można zastosować do trwałej właściwości lub zmiennej instancji dowolnego z następujących typów: typy pierwotne Java, [...], wyliczenia i każdy inny typ, który implementuje java.io.Serializable.

Ważną częścią jest typ, który implementuje Serializable

Zdecydowanie najprostszym i najłatwiejszym w użyciu rozwiązaniem jest po prostu użycie ArrayList zamiast List (lub dowolnego serializowalnego kontenera):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Pamiętaj jednak, że użyje to serializacji systemu, więc będzie to miało pewną cenę, taką jak:

  • jeśli serializowany model obiektów ulegnie zmianie, przywrócenie danych może nie być możliwe

  • mały narzut jest dodawany do każdego przechowywanego elementu.

W skrócie

przechowywanie flag lub kilku elementów jest dość proste, ale nie polecałbym tego do przechowywania danych, które mogą się rozrosnąć.


próbowałem tego, ale tabela sql sprawiła, że ​​typ danych to tinyblob. Czy to nie sprawia, że ​​wstawianie i pobieranie listy ciągów jest bardzo niewygodne? A może jpa automatycznie serializuje i deserializuje dla Ciebie?
Dzhao

3

Odpowiedź Thiago jest poprawna, dodając próbkę bardziej szczegółową do pytania, @ElementCollection utworzy nową tabelę w twojej bazie danych, ale bez mapowania dwóch tabel, Oznacza to, że kolekcja nie jest zbiorem encji, ale zbiorem prostych typów (ciągi znaków itp. .) lub zbiór elementów do osadzania (klasa z adnotacją @Embeddable ).

Oto przykład do utrwalenia listy String

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

Oto przykład do utrwalenia listy obiektów niestandardowych

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

W tym przypadku musimy uczynić klasę Embeddable

@Embeddable
public class Car {
}

3

Oto rozwiązanie do przechowywania zestawu przy użyciu @Converter i StringTokenizer. Trochę więcej testów z rozwiązaniem @ jonck-van-der-kogel .

W swojej klasie 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

Moim rozwiązaniem tego problemu było oddzielenie klucza podstawowego od klucza obcego. Jeśli używasz eclipse i dokonałeś powyższych zmian, pamiętaj o odświeżeniu eksploratora bazy danych. Następnie ponownie utwórz jednostki z tabel.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.