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.