Wie kann eine Eigenschaft vom Typ List <String> in JPA beibehalten werden?

158

Was ist der intelligenteste Weg, um eine Entität mit einem Feld vom Typ Liste dauerhaft zu erhalten?

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

Dieser Code erzeugt:

> 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: 
> ...
Andrea Francia
quelle

Antworten:

197

Verwenden Sie eine JPA 2-Implementierung: Sie fügt eine @ ElementCollection-Annotation hinzu, die der Hibernate-Annotation ähnelt und genau das tut, was Sie benötigen. Es gibt ein Beispiel hier .

Bearbeiten

Wie in den Kommentaren unten erwähnt, ist die korrekte JPA 2-Implementierung

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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

Thiago H. de Paula Figueiredo
quelle
1
Mein Fehler war, auch @ OneToMany-Annotation hinzuzufügen ... nachdem ich sie entfernt und gerade @ ElementCollection verlassen hatte, funktionierte es
Willi Mentzel
46

Es tut mir leid, einen alten Thread wiederzubeleben, aber sollte jemand nach einer alternativen Lösung suchen, bei der Sie Ihre Zeichenfolgenlisten als ein Feld in Ihrer Datenbank speichern, habe ich das folgendermaßen gelöst. Erstellen Sie einen Konverter wie folgt:

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

Verwenden Sie es jetzt wie folgt für Ihre Entitäten:

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

In der Datenbank wird Ihre Liste als foo; bar; foobar gespeichert und in Ihrem Java-Objekt erhalten Sie eine Liste mit diesen Zeichenfolgen.

Hoffe das ist hilfreich für jemanden.

Jonck van der Kogel
quelle
Funktioniert es mit jpa-Repositorys, um Ergebnisse nach Inhalten dieses Feldes zu filtern?
Please_Dont_Bully_Me_SO_Lords
1
@Please_Dont_Bully_Me_SO_Lords Es ist für diesen Anwendungsfall weniger geeignet, da Ihre Daten in der Datenbank als "foo; bar; foobar" gespeichert werden. Wenn Sie die Daten abfragen möchten, ist wahrscheinlich eine ElementCollection + JoinTable der richtige Weg für Ihre Situation.
Jonck van der Kogel
Dies bedeutet auch, dass SPLIT_CHARIhre Zeichenfolge keine Vorkommen enthalten kann.
Crush
@crush das ist richtig. Natürlich können Sie dies zulassen, indem Sie beispielsweise Ihre Zeichenfolge codieren, nachdem Sie sie korrekt abgegrenzt haben. Die hier veröffentlichte Lösung ist jedoch hauptsächlich für einfache Anwendungsfälle gedacht. Für komplexere Situationen werden Sie wahrscheinlich mit einer ElementCollection + JoinTable
Jonck van der Kogel am
Bitte korrigieren Sie Ihren Code. Ich betrachte es als 'Bibliothekscode', also sollte es defensiv sein, z. B. sollte es zumindest
ZZ 5
30

Diese Antwort wurde vor JPA2-Implementierungen gegeben. Wenn Sie JPA2 verwenden, lesen Sie die Antwort von ElementCollection oben:

Listen von Objekten innerhalb eines Modellobjekts werden im Allgemeinen als "OneToMany" -Beziehungen zu einem anderen Objekt betrachtet. Ein String ist jedoch (für sich genommen) kein zulässiger Client einer Eins-zu-Viele-Beziehung, da er keine ID hat.

Also, Sie sollten Ihre Liste von Strings auf eine Liste von Argument-Klasse JPA konvertieren Objekte enthält eine ID und ein String. Sie könnten möglicherweise den String als ID verwenden, wodurch ein wenig Platz in Ihrer Tabelle gespart wird, sowohl durch Entfernen des ID-Felds als auch durch Konsolidieren von Zeilen, in denen die Strings gleich sind. Sie würden jedoch die Möglichkeit verlieren, die Argumente wieder in ihre ursprüngliche Reihenfolge zu bringen (da Sie keine Bestellinformationen gespeichert haben).

Alternativ können Sie Ihre Liste in @Transient konvertieren und Ihrer Klasse ein weiteres Feld (argStorage) hinzufügen, das entweder ein VARCHAR () oder ein CLOB ist. Sie müssen dann 3 Funktionen hinzufügen: 2 davon sind identisch und sollten Ihre Liste der Zeichenfolgen in eine einzelne Zeichenfolge (in argStorage) konvertieren, die so abgegrenzt ist, dass Sie sie leicht trennen können. Kommentieren Sie diese beiden Funktionen (die jeweils dasselbe tun) mit @PrePersist und @PreUpdate. Fügen Sie abschließend die dritte Funktion hinzu, mit der argStorage erneut in die Liste der Zeichenfolgen aufgeteilt wird, und kommentieren Sie sie mit @PostLoad. Dadurch wird Ihr CLOB immer mit den Zeichenfolgen aktualisiert, wenn Sie den Befehl speichern, und das Feld argStorage wird aktualisiert, bevor Sie es in der Datenbank speichern.

Ich schlage immer noch vor, den ersten Fall zu machen. Es ist eine gute Praxis für echte Beziehungen später.

billjamesdev
quelle
Der Wechsel von ArrayList <String> zu String mit durch Kommas getrennten Werten hat bei mir funktioniert.
Chris Dale
2
Dies zwingt Sie jedoch dazu, (imho) hässliche Aussagen zu verwenden, wenn Sie dieses Feld abfragen.
Whiskysierra
Ja, wie gesagt ... mach die erste Option, es ist besser. Wenn Sie sich einfach nicht dazu bringen können, kann Option 2 funktionieren.
Billjamesdev
15

Laut Java Persistence with Hibernate

Zuordnen von Sammlungen von Werttypen mit Anmerkungen [...]. Zum Zeitpunkt des Schreibens ist es nicht Teil des Java Persistence-Standards

Wenn Sie den Ruhezustand verwenden, können Sie Folgendes tun:

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

Update: Beachten Sie, dass dies jetzt in JPA2 verfügbar ist.

Toolkit
quelle
12

Wir können dies auch nutzen.

@Column(name="arguments")
@ElementCollection(targetClass=String.class)
private List<String> arguments;
Jaimin Patel
quelle
1
wahrscheinlich plus @JoinTable.
Phil294
9

Bei der Verwendung der JPA-Implementierung im Ruhezustand habe ich festgestellt, dass durch einfaches Deklarieren des Typs als ArrayList anstelle von List die Liste der Daten im Ruhezustand gespeichert werden kann.

Dies hat eindeutig eine Reihe von Nachteilen gegenüber dem Erstellen einer Liste von Entitätsobjekten. Kein verzögertes Laden, keine Möglichkeit, die Entitäten in der Liste von anderen Objekten aus zu referenzieren, möglicherweise größere Schwierigkeiten beim Erstellen von Datenbankabfragen. Wenn Sie sich jedoch mit Listen ziemlich primitiver Typen befassen, die Sie immer eifrig zusammen mit der Entität abrufen möchten, scheint mir dieser Ansatz in Ordnung zu sein.

@Entity
public class Command implements Serializable {

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

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


}

quelle
2
Vielen Dank. Diese Arbeit mit allen JPA-Implementierungen, Arraylist is Serializable, wird in einem BLOB-Feld gespeichert. Die Probleme bei dieser Methode sind, dass 1) die BLOB-Größe festgelegt ist 2) Sie die Array-Elemente suchen oder indizieren können 3) nur ein Client, der das Java-Serialisierungsformat kennt, diese Elemente lesen kann.
Andrea Francia
Wenn Sie diesen Ansatz @OneToMany @ManyToOne @ElementCollectiondamit versuchen , erhalten Sie Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementsbeim Serverstart eine Ausnahme. Weil der Ruhezustand möchte, dass Sie Erfassungsschnittstellen verwenden.
Paramvir Singh Karwal
9

Ich hatte das gleiche Problem, also habe ich die mögliche Lösung investiert, aber am Ende habe ich beschlossen, mein ';' getrennte Liste von String.

Also habe ich

// a ; separated list of arguments
String arguments;

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

Auf diese Weise ist die Liste in der Datenbanktabelle leicht lesbar / bearbeitbar.

Anthony
quelle
1
Dies ist völlig gültig, berücksichtigen Sie jedoch das Wachstum Ihrer Anwendung und die Schemaentwicklung. Irgendwann in (naher) Zukunft könnten Sie irgendwann zum entitätsbasierten Ansatz wechseln.
whiskeysierra
Ich stimme zu, das ist absolut gültig. Ich empfehle jedoch, die Logik sowie die Implementierung des Codes vollständig zu überprüfen. Wenn String argumentseine Liste von Zugriffsberechtigungen ist, kann ein Sonderzeichen a separatorfür Angriffe zur Eskalation von Berechtigungen anfällig sein.
Thang Pham
1
Dies ist ein wirklich schlechter Rat. Ihre Zeichenfolge enthält möglicherweise Informationen, ;die Ihre App beschädigen.
Agilob
9

Es scheint, dass keine der Antworten die wichtigsten Einstellungen für ein @ElementCollectionMapping untersucht hat.

Wenn Sie eine Liste mit dieser Anmerkung zuordnen und JPA / Hibernate die Tabellen, Spalten usw. automatisch generieren lässt, werden auch automatisch generierte Namen verwendet.

Analysieren wir also ein grundlegendes Beispiel:

@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. Die grundlegende @ElementCollectionAnmerkung (wo Sie die bekannten fetchund targetClassEinstellungen definieren können )
  2. Die @CollectionTableAnmerkung ist sehr nützlich , wenn es einen Namen für die Tabelle geben kommt , die erzeugt werden , werden, sowie Definitionen wie joinColumns, foreignKey‚s, indexes, uniqueConstraintsetc.
  3. @ColumnEs ist wichtig, den Namen der Spalte zu definieren, in der der varcharWert der Liste gespeichert wird .

Die generierte DDL-Erstellung wäre:

-- 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)
);
Bosco
quelle
4
Ich mag diese Lösung, weil sie die einzige vorgeschlagene Lösung ist, die die vollständige Beschreibung einschließlich der TABLE-Strukturen enthält und erklärt, warum wir die verschiedenen Anmerkungen benötigen.
Julien Kronegg
6

Ok, ich weiß, dass es etwas spät ist. Aber für jene tapferen Seelen, die dies im Laufe der Zeit sehen werden.

Wie in der Dokumentation geschrieben :

@Basic: Die einfachste Art der Zuordnung zu einer Datenbankspalte. Die Basic-Annotation kann auf eine persistente Eigenschaft oder Instanzvariable eines der folgenden Typen angewendet werden: Java-Grundtypen, [...], Aufzählungen und jeder andere Typ, der java.io.Serializable implementiert.

Der wichtige Teil ist der Typ, der Serializable implementiert

Die mit Abstand einfachste und am einfachsten zu verwendende Lösung ist die Verwendung von ArrayList anstelle von List (oder eines beliebigen serialisierbaren Containers):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Denken Sie jedoch daran, dass hierfür die System-Serialisierung verwendet wird, sodass ein gewisser Preis anfällt, z.

  • Wenn sich das serialisierte Objektmodell ändert, können Sie möglicherweise keine Daten wiederherstellen

  • Für jedes gespeicherte Element wird ein geringer Overhead hinzugefügt.

Zusamenfassend

Es ist ganz einfach, Flags oder wenige Elemente zu speichern, aber ich würde es nicht empfehlen, Daten zu speichern, die groß werden könnten.

Inverce
quelle
versuchte dies, aber die SQL-Tabelle machte den Datentyp zu einem winzigen Blob. Ist das Einfügen und Abrufen der Liste der Zeichenfolgen dadurch nicht sehr unpraktisch? Oder serialisiert und deserialisiert jpa automatisch für Sie?
Dzhao
3

Diese Antwort ist richtig. Wenn Sie ein Beispiel hinzufügen, das spezifischer für die Frage ist, erstellt @ElementCollection eine neue Tabelle in Ihrer Datenbank, ohne jedoch zwei Tabellen zuzuordnen. Dies bedeutet, dass die Sammlung keine Sammlung von Entitäten ist, sondern eine Sammlung einfacher Typen (Strings usw.) .) oder eine Sammlung einbettbarer Elemente (mit @Embeddable kommentierte Klasse ).

Hier ist das Beispiel, um die Liste der Zeichenfolgen beizubehalten

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

Hier ist das Beispiel, um die Liste der benutzerdefinierten Objekte beizubehalten

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

Für diesen Fall müssen wir die Klasse Embeddable machen

@Embeddable
public class Car {
}
Zia
quelle
3

Hier ist die Lösung zum Speichern eines Sets mit @Converter und StringTokenizer. Ein bisschen mehr Überprüfungen gegen @ jonck-van-der-kogel- Lösung.

In Ihrer Entitätsklasse:

@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;
    }
}
gosuer1921
quelle
1

Meine Lösung für dieses Problem bestand darin, den Primärschlüssel vom Fremdschlüssel zu trennen. Wenn Sie Eclipse verwenden und die oben genannten Änderungen vorgenommen haben, müssen Sie den Datenbank-Explorer aktualisieren. Erstellen Sie dann die Entitäten aus den Tabellen neu.

treibt Fleisch an
quelle