Come mantenere una proprietà di tipo Elenco <String> in JPA?


158

Qual è il modo più intelligente per ottenere un'entità con un campo di tipo Elenco persistente?

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

Questo codice produce:

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

Risposte:


197

Usa alcune implementazioni di JPA 2: aggiunge un'annotazione @ElementCollection, simile a quella di Hibernate, che fa esattamente quello che ti serve. C'è un esempio qui .

modificare

Come menzionato nei commenti seguenti, l'implementazione corretta di JPA 2 è

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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


1
Il mio errore è stato aggiungere anche l'annotazione @ OneToMany ... dopo averla rimossa e aver lasciato @ ElementCollection ha funzionato
Willi Mentzel,

47

Mi dispiace rivivere un vecchio thread, ma se qualcuno dovesse cercare una soluzione alternativa in cui archiviare gli elenchi di stringhe come un campo nel database, ecco come l'ho risolto. Crea un convertitore come questo:

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

Ora usalo sulle tue Entità in questo modo:

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

Nel database la tua lista sarà memorizzata come foo; bar; foobar e nel tuo oggetto Java otterrai una lista con quelle stringhe.

Spero che questo sia utile a qualcuno.


Funzionerà con i repository jpa per filtrare i risultati in base al contenuto di quel campo?
Please_Dont_Bully_Me_SO_Lords

1
@Please_Dont_Bully_Me_SO_Lords È meno adatto per quel caso d'uso poiché i tuoi dati saranno nel database come "foo; bar; foobar". Se si desidera eseguire una query per i dati, probabilmente ElementCollection + JoinTable è la strada da percorrere per la propria situazione.
Jonck van der Kogel

Questo significa anche che non puoi avere SPLIT_CHARoccorrenze nella tua stringa.
schiaccia il

@crush che è corretto. Anche se ovviamente potresti permetterlo, ad esempio codificando la tua stringa dopo averla delimitata correttamente. Ma la soluzione che ho pubblicato qui è principalmente pensata per casi d'uso semplici; per situazioni più complesse probabilmente andrai meglio con ElementCollection + JoinTable
Jonck van der Kogel

Si prega di correggere il codice. Lo considero un "codice di libreria", quindi dovrebbe essere difensivo, ad esempio almeno dovrebbe avere controlli nulli
ZZ 5

30

Questa risposta è stata realizzata implementazioni pre-JPA2, se si utilizza JPA2, vedere la risposta ElementCollection sopra:

Gli elenchi di oggetti all'interno di un oggetto modello sono generalmente considerati relazioni "OneToMany" con un altro oggetto. Tuttavia, una stringa non è (di per sé) un client consentito di una relazione uno-a-molti, poiché non ha un ID.

Quindi dovresti convertire l'elenco di stringhe in un elenco di oggetti JPA di classe Argument contenenti un ID e una stringa. Potresti potenzialmente utilizzare String come ID, che risparmierebbe un po 'di spazio nella tabella sia rimuovendo il campo ID che consolidando le righe in cui le stringhe sono uguali, ma perderesti la possibilità di riordinare gli argomenti nel loro ordine originale (poiché non hai memorizzato alcuna informazione di ordinazione).

In alternativa, puoi convertire il tuo elenco in @Transient e aggiungere un altro campo (argStorage) alla tua classe che sia un VARCHAR () o un CLOB. Dovrai quindi aggiungere 3 funzioni: 2 di esse sono uguali e dovrebbero convertire il tuo elenco di stringhe in una singola stringa (in argStorage) delimitata in modo da poterle separare facilmente. Annota queste due funzioni (che fanno ciascuna la stessa cosa) con @PrePersist e @PreUpdate. Infine, aggiungi la terza funzione che divide di nuovo argStorage nell'elenco di stringhe e annotalo @PostLoad. Ciò manterrà il CLOB aggiornato con le stringhe ogni volta che andrai a memorizzare il comando e manterrà aggiornato il campo argStorage prima di memorizzarlo nel DB.

Consiglio ancora di fare il primo caso. È buona pratica per relazioni vere in seguito.


Il passaggio da ArrayList <String> a String con valori separati da virgola ha funzionato per me.
Chris Dale,

2
Ma questo ti costringe a usare (imho) brutte istruzioni come quando interroghi quel campo.
whiskeysierra,

Sì, come ho detto ... fai la prima opzione, è meglio. Se proprio non riesci a farlo, l'opzione 2 può funzionare.
billjamesdev,

15

Secondo Java Persistence with Hibernate

mappatura di raccolte di tipi di valore con annotazioni [...]. Al momento della stesura non fa parte dello standard Java Persistence

Se stavi usando Hibernate, potresti fare qualcosa del tipo:

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

Aggiornamento: Nota, questo è ora disponibile in JPA2.


12

Possiamo anche usare questo.

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

1
probabilmente più @JoinTable.
phil294

10

Ho avuto lo stesso problema, quindi ho investito la possibile soluzione fornita ma alla fine ho deciso di implementare il mio ';' elenco separato di String.

quindi ho

// a ; separated list of arguments
String arguments;

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

In questo modo l'elenco è facilmente leggibile / modificabile nella tabella del database;


1
Questo è totalmente valido ma considera la crescita della tua applicazione e l'evoluzione dello schema. In un futuro (prossimo) potresti eventualmente passare all'approccio basato sull'entità.
whiskeysierra,

Sono d'accordo, è totalmente valido. Tuttavia, suggerisco di rivedere completamente la logica e l'implementazione del codice. Se String argumentsè un elenco di autorizzazioni di accesso, quindi con un carattere speciale, a separator, potrebbe essere vulnerabile agli attacchi di escalation di privilegi.
Thang Pham,

1
Questo è davvero un cattivo consiglio, la tua stringa potrebbe contenere ;ciò che romperà la tua app.
agilob,

9

Quando ho utilizzato l'implementazione Hibernate di JPA, ho scoperto che la semplice dichiarazione del tipo come ArrayList invece di List consente a hibernate di memorizzare l'elenco di dati.

Chiaramente questo presenta una serie di svantaggi rispetto alla creazione di un elenco di oggetti Entity. Nessun caricamento lento, nessuna possibilità di fare riferimento alle entità nell'elenco da altri oggetti, forse più difficoltà nella costruzione di query del database. Tuttavia, quando hai a che fare con elenchi di tipi abbastanza primitivi che vorrai sempre prendere con entusiasmo insieme all'entità, allora questo approccio mi sembra perfetto.

@Entity
public class Command implements Serializable {

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

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


}

2
Grazie. Questo lavoro con tutte le implementazioni di JPA, Arraylist è serializzabile, viene salvato in un campo BLOB. I problemi con questo metodo sono che 1) la dimensione BLOB è fissa 2) è possibile cercare o indicizzare gli elementi dell'array 3) solo un client consapevole del formato di serializzazione Java può leggere questi elementi.
Andrea Francia,

Nel caso in cui provi questo approccio con @OneToMany @ManyToOne @ElementCollectionesso ti darà Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementseccezione all'avvio del server. Perché ibernazione vuole che tu usi le interfacce di raccolta.
Paramvir Singh Karwal,

9

Sembra che nessuna delle risposte abbia esplorato le impostazioni più importanti per a @ElementCollection mappatura.

Quando si mappa un elenco con questa annotazione e si consente a JPA / Hibernate di generare automaticamente tabelle, colonne e così via, verranno utilizzati anche i nomi generati automaticamente.

Quindi, analizziamo un esempio di base:

@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. L' @ElementCollectionannotazione di base (dove è possibile definire il noto fetchetargetClass preferenze)
  2. L' @CollectionTableannotazione è molto utile quando si tratta di dare un nome al tavolo che sarà generato, oltre che definizioni come joinColumns, foreignKey's, indexes,uniqueConstraints , etc.
  3. @Columnè importante definire il nome della colonna che memorizzerà il varcharvalore dell'elenco.

La creazione DDL generata sarebbe:

-- 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
Mi piace questa soluzione perché è l'unica soluzione proposta che fornisce la descrizione completa, comprese le strutture TABELLA, e spiega perché abbiamo bisogno delle diverse annotazioni.
Julien Kronegg,

6

Ok, lo so che è un po 'tardi. Ma per quelle anime coraggiose che vedranno questo col passare del tempo.

Come scritto nella documentazione :

@Basic: il tipo più semplice di mappatura su una colonna del database. L'annotazione di base può essere applicata a una proprietà persistente o a una variabile di istanza di uno dei seguenti tipi: tipi primitivi Java, [...], enum e qualsiasi altro tipo che implementa java.io.Serializable.

La parte importante è tipo che implementa Serializable

Quindi la soluzione di gran lunga più semplice e facile da usare è semplicemente usare ArrayList invece di List (o qualsiasi contenitore serializzabile):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Ricorda tuttavia che questo utilizzerà la serializzazione del sistema, quindi avrà un prezzo, come ad esempio:

  • se il modello a oggetti serializzato cambierà, potresti non essere in grado di ripristinare i dati

  • viene aggiunto un piccolo overhead per ogni elemento memorizzato.

In breve

è abbastanza semplice memorizzare flag o pochi elementi, ma non lo consiglierei per archiviare dati che potrebbero diventare grandi.


provato questo, ma la tabella sql ha reso il tipo di dati un piccolissimo blob. Ciò non rende molto scomodo l'inserimento e il recupero dell'elenco di stringhe? Oppure jpa serializza e deserializza automaticamente per te?
Dzhao,

3

La risposta di Thiago è corretta, aggiungendo un campione più specifico alla domanda, @ElementCollection creerà una nuova tabella nel database, ma senza mappare due tabelle, significa che la raccolta non è una raccolta di entità, ma una raccolta di tipi semplici (stringhe, ecc. .) o una raccolta di elementi incorporabili (classe annotata con @Embeddable ).

Ecco l'esempio per persistere nell'elenco di String

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

Ecco l'esempio per persistere nell'elenco di oggetti personalizzati

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

Per questo caso dobbiamo rendere Embeddable di classe

@Embeddable
public class Car {
}

3

Ecco la soluzione per memorizzare un set usando @Converter e StringTokenizer. Un altro po 'di controlli sulla soluzione @ Jonck-Van-der-Kogel .

Nella tua classe 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

La mia correzione per questo problema era di separare la chiave primaria con la chiave esterna. Se si utilizza eclipse e sono state apportate le modifiche sopra riportate, ricordarsi di aggiornare Database Explorer. Quindi ricreare le entità dalle tabelle.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.