Comment conserver une propriété de type List <String> dans JPA?

158

Quelle est la manière la plus intelligente d'obtenir une entité avec un champ de type Liste persistante?

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

Ce code produit:

> 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
la source

Réponses:

197

Utilisez une implémentation JPA 2: elle ajoute une annotation @ElementCollection, similaire à celle d'Hibernate, qui fait exactement ce dont vous avez besoin. Il y a un exemple ici .

Éditer

Comme mentionné dans les commentaires ci-dessous, la mise en œuvre correcte de JPA 2 est

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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

Thiago H. de Paula Figueiredo
la source
1
Mon erreur a été d'ajouter l'annotation @ OneToMany également ... après l'avoir supprimée et juste de quitter @ ElementCollection, cela a fonctionné
Willi Mentzel
47

Désolé de relancer un ancien fil de discussion, mais si quelqu'un cherche une solution alternative où vous stockez vos listes de chaînes dans un champ de votre base de données, voici comment j'ai résolu cela. Créez un convertisseur comme celui-ci:

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

Maintenant, utilisez-le sur vos entités comme ceci:

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

Dans la base de données, votre liste sera stockée sous la forme foo; bar; foobar et dans votre objet Java, vous obtiendrez une liste avec ces chaînes.

J'espère que cela est utile à quelqu'un.

Jonck van der Kogel
la source
Fonctionnera-t-il avec les référentiels jpa pour filtrer les résultats par contenu de ce champ?
Please_Dont_Bully_Me_SO_Lords
1
@Please_Dont_Bully_Me_SO_Lords C'est moins approprié pour ce cas d'utilisation puisque vos données seront dans la base de données sous la forme "foo; bar; foobar". Si vous souhaitez interroger les données, un ElementCollection + JoinTable est probablement la solution pour votre situation.
Jonck van der Kogel
Cela signifie également que vous ne pouvez pas avoir d' SPLIT_CHARoccurrences dans votre chaîne.
écraser
@crush c'est correct. Bien sûr, vous pouvez le permettre par exemple en encodant votre chaîne après l'avoir correctement délimitée. Mais la solution que j'ai publiée ici est principalement destinée à des cas d'utilisation simples; pour des situations plus complexes, vous vous en sortirez probablement mieux avec une ElementCollection + JoinTable
Jonck van der Kogel
Veuillez corriger votre code. Je le considère comme un 'code de bibliothèque', il devrait donc être défensif, par exemple au moins il devrait avoir des vérifications nulles
ZZ 5
30

Cette réponse a été faite avec des implémentations pré-JPA2, si vous utilisez JPA2, voir la réponse ElementCollection ci-dessus:

Les listes d'objets à l'intérieur d'un objet modèle sont généralement considérées comme des relations «OneToMany» avec un autre objet. Cependant, une chaîne n'est pas (par elle-même) un client autorisé d'une relation un-à-plusieurs, car elle n'a pas d'ID.

Alors tu devrais convertir votre liste de chaînes à une liste de JPA Argument classe Les objets contenant un identifiant et une chaîne. Vous pourriez potentiellement utiliser la chaîne comme ID, ce qui économiserait un peu d'espace dans votre table à la fois en supprimant le champ ID et en consolidant les lignes où les chaînes sont égales, mais vous perdriez la possibilité de réorganiser les arguments dans leur ordre d'origine (car vous n'avez stocké aucune information de commande).

Vous pouvez également convertir votre liste en @Transient et ajouter un autre champ (argStorage) à votre classe qui est un VARCHAR () ou un CLOB. Vous devrez ensuite ajouter 3 fonctions: 2 d'entre elles sont identiques et devraient convertir votre liste de chaînes en une seule chaîne (dans argStorage) délimitée de manière à pouvoir les séparer facilement. Annotez ces deux fonctions (qui font chacune la même chose) avec @PrePersist et @PreUpdate. Enfin, ajoutez à nouveau la troisième fonction qui divise argStorage dans la liste des chaînes et annotez-la @PostLoad. Cela gardera votre CLOB mis à jour avec les chaînes chaque fois que vous allez stocker la commande et gardera le champ argStorage mis à jour avant de le stocker dans la base de données.

Je suggère toujours de faire le premier cas. C'est une bonne pratique pour de vraies relations plus tard.

billjamesdev
la source
Passer de ArrayList <String> à String avec des valeurs séparées par des virgules a fonctionné pour moi.
Chris Dale
2
Mais cela vous oblige à utiliser (à mon humble avis) des déclarations similaires lors de l'interrogation de ce champ.
whiskeysierra
Oui, comme je l'ai dit ... faites la première option, c'est mieux. Si vous ne pouvez tout simplement pas vous résoudre à le faire, l'option 2 peut fonctionner.
billjamesdev
15

Selon Java Persistence avec Hibernate

mapper des collections de types de valeur avec des annotations [...]. Au moment de la rédaction de cet article, il ne fait pas partie du standard Java Persistence

Si vous utilisiez Hibernate, vous pourriez faire quelque chose comme:

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

Mise à jour: Notez que ceci est maintenant disponible dans JPA2.

boîte à outils
la source
12

Nous pouvons également l'utiliser.

@Column(name="arguments")
@ElementCollection(targetClass=String.class)
private List<String> arguments;
Jaimin Patel
la source
1
probablement plus @JoinTable.
phil294
10

J'avais le même problème donc j'ai investi la solution possible donnée mais à la fin j'ai décidé de mettre en œuvre mon ';' liste séparée de String.

donc j'ai

// a ; separated list of arguments
String arguments;

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

De cette façon, la liste est facilement lisible / modifiable dans la table de la base de données;

Anthony
la source
1
Ceci est tout à fait valable mais considérez la croissance de votre application et l'évolution du schéma. Dans un futur (proche), vous pourriez éventuellement passer à l'approche basée sur les entités.
whiskeysierra
Je suis d'accord, c'est tout à fait valable. Cependant, je suggère de revoir complètement la logique ainsi que la mise en œuvre du code. Si String argumentsest une liste d'autorisations d'accès, le caractère spécial, a separator, peut être vulnérable aux attaques d'escalade de privilèges.
Thang Pham
1
C'est vraiment un mauvais conseil, votre chaîne peut contenir ;ce qui cassera votre application.
agilob
9

Lors de l'utilisation de l'implémentation Hibernate de JPA, j'ai constaté que le simple fait de déclarer le type en tant que ArrayList au lieu de List permet à hibernate de stocker la liste des données.

De toute évidence, cela présente un certain nombre d'inconvénients par rapport à la création d'une liste d'objets Entity. Pas de chargement paresseux, pas de possibilité de référencer les entités de la liste à partir d'autres objets, peut-être plus de difficulté à construire des requêtes de base de données. Cependant, lorsque vous avez affaire à des listes de types assez primitifs que vous voudrez toujours chercher avec impatience avec l'entité, alors cette approche me semble bien.

@Entity
public class Command implements Serializable {

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

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


}

la source
2
Merci. Ce travail avec toutes les implémentations JPA, Arraylist is Serializable est enregistré dans un champ BLOB. Les problèmes avec cette méthode sont que 1) la taille BLOB est fixe 2) vous pouvez rechercher ou indexer les éléments du tableau 3) seul un client connaissant le format de sérialisation Java peut lire ces éléments.
Andrea Francia
Si vous essayez cette approche, @OneToMany @ManyToOne @ElementCollectioncela vous donnera une Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementsexception au démarrage du serveur. Parce que hibernates veut que vous utilisiez des interfaces de collecte.
Paramvir Singh Karwal
9

Il semble qu'aucune des réponses n'ait exploré les paramètres les plus importants pour un @ElementCollection mappage.

Lorsque vous mappez une liste avec cette annotation et que JPA / Hibernate génère automatiquement les tables, les colonnes, etc., il utilisera également des noms générés automatiquement.

Alors, analysons un exemple de 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' @ElementCollectionannotation de base (où vous pouvez définir lefetch ettargetClass préférences)
  2. L' @CollectionTableannotation est très utile quand il s'agit de donner un nom à la table qui va générer, ainsi que des définitions comme celle joinColumns, foreignKeyl », indexes,uniqueConstraints , etc.
  3. @Columnest important de définir le nom de la colonne qui stockera la varcharvaleur de la liste.

La création DDL générée serait:

-- 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
la source
4
J'aime cette solution car c'est la seule solution proposée qui donne la description complète y compris les structures TABLE et explique pourquoi nous avons besoin des différentes annotations.
Julien Kronegg le
6

Ok je sais que c'est un peu tard. Mais pour ces âmes courageuses qui verront cela avec le temps.

Comme écrit dans la documentation :

@Basic: le type le plus simple de mappage vers une colonne de base de données. L'annotation de base peut être appliquée à une propriété persistante ou à une variable d'instance de l'un des types suivants: types primitifs Java, [...], énumérations et tout autre type qui implémente java.io.Serializable.

La partie importante est type qui implémente Serializable

Donc, de loin, la solution la plus simple et la plus facile à utiliser consiste simplement à utiliser ArrayList au lieu de List (ou de tout conteneur sérialisable):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Rappelez-vous cependant que cela utilisera la sérialisation du système, donc cela viendra avec un certain prix, tel que:

  • si le modèle d'objet sérialisé change, vous ne pourrez peut-être pas restaurer les données

  • une petite surcharge est ajoutée pour chaque élément stocké.

En bref

il est assez simple de stocker des drapeaux ou quelques éléments, mais je ne le recommanderais pas pour stocker des données qui pourraient grossir.

Inverce
la source
essayé, mais la table sql a fait du type de données un tinyblob. Cela ne rend-il pas l'insertion et la récupération de la liste de chaînes très gênantes? Ou est-ce que jpa sérialise et désérialise automatiquement pour vous?
Dzhao
3

La réponse de Thiago est correcte, en ajoutant un échantillon plus spécifique à la question, @ElementCollection créera une nouvelle table dans votre base de données, mais sans mapper deux tables, cela signifie que la collection n'est pas une collection d'entités, mais une collection de types simples (chaînes, etc. .) ou une collection d'éléments intégrables (classe annotée avec @Embeddable ).

Voici l'exemple pour conserver la liste de String

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

Voici l'exemple pour conserver la liste des objets personnalisés

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

Pour ce cas, nous devons rendre la classe Embeddable

@Embeddable
public class Car {
}
Zia
la source
3

Voici la solution pour stocker un Set à l'aide de @Converter et StringTokenizer. Un peu plus de vérifications par rapport à la solution @ jonck-van-der-kogel .

Dans votre 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;
    }
}
gosuer1921
la source
1

Mon correctif pour ce problème était de séparer la clé primaire avec la clé étrangère. Si vous utilisez eclipse et avez apporté les modifications ci-dessus, n'oubliez pas d'actualiser l'explorateur de base de données. Puis recréez les entités à partir des tables.

alimente la viande
la source