¿Cómo persistir una propiedad de tipo List <String> en JPA?


158

¿Cuál es la forma más inteligente de lograr que una entidad con un campo de tipo Lista persista?

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

Este código 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: 
> ...

Respuestas:


197

Use alguna implementación de JPA 2: agrega una anotación @ElementCollection, similar a la de Hibernate, que hace exactamente lo que necesita. Hay un ejemplo aquí .

Editar

Como se menciona en los comentarios a continuación, la implementación correcta de JPA 2 es

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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


1
Mi error fue agregar también la anotación @ OneToMany ... después de eliminarla y simplemente dejar @ ElementCollection funcionó
Willi Mentzel

47

Lamento revivir un hilo antiguo, pero si alguien busca una solución alternativa donde almacene sus listas de cadenas como un campo en su base de datos, así es como lo resolví. Crea un convertidor como este:

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

Ahora úsalo en tus Entidades así:

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

En la base de datos, su lista se almacenará como foo; bar; foobar y en su objeto Java obtendrá una lista con esas cadenas.

Espero que esto sea útil para alguien.


¿Funcionará con repositorios jpa para filtrar resultados por contenido de ese campo?
Please_Dont_Bully_Me_SO_Lords

1
@Please_Dont_Bully_Me_SO_Lords Es menos adecuado para ese caso de uso ya que sus datos estarán en la base de datos como "foo; bar; foobar". Si desea consultar los datos, entonces probablemente una ElementCollection + JoinTable es el camino a seguir para su situación.
Jonck van der Kogel

Esto también significa que no puede tener ninguna SPLIT_CHARocurrencia en su cadena.
aplastar

@crush eso es correcto. Aunque, por supuesto, podría permitirlo codificando, por ejemplo, su cadena después de haberla delimitado correctamente. Pero la solución que publiqué aquí está destinada principalmente a casos de uso simples; para situaciones más complejas, probablemente le irá mejor con ElementCollection + JoinTable
Jonck van der Kogel

Por favor arregle su código. Lo considero como 'código de biblioteca', por lo que debería ser defensivo, por ejemplo, al menos debería tener comprobaciones nulas
ZZ 5

30

Esta respuesta se realizó antes de las implementaciones de JPA2, si está utilizando JPA2, consulte la respuesta de ElementCollection anterior:

Las listas de objetos dentro de un objeto modelo generalmente se consideran relaciones "OneToMany" con otro objeto. Sin embargo, una Cadena no es (por sí misma) un cliente permitido de una relación Uno a Muchos, ya que no tiene una ID.

Por lo tanto, debe convertir su lista de cadenas en una lista de objetos JPA de clase Argumento que contengan un ID y una cadena. Potencialmente, podría usar la Cadena como ID, lo que ahorraría un poco de espacio en su tabla, tanto al eliminar el campo ID como al consolidar filas donde las Cadenas son iguales, pero perdería la capacidad de ordenar los argumentos nuevamente en su orden original (ya que no almacenó ninguna información de pedido).

Alternativamente, puede convertir su lista a @Transient y agregar otro campo (argStorage) a su clase que sea VARCHAR () o CLOB. Luego deberá agregar 3 funciones: 2 de ellas son las mismas y deben convertir su lista de cadenas en una sola cadena (en argStorage) delimitada de manera que pueda separarlas fácilmente. Anote estas dos funciones (que cada una hace lo mismo) con @PrePersist y @PreUpdate. Finalmente, agregue la tercera función que divide el argStorage en la lista de cadenas nuevamente y anótelo @PostLoad. Esto mantendrá su CLOB actualizado con las cadenas cada vez que vaya a almacenar el comando, y mantendrá actualizado el campo argStorage antes de almacenarlo en la base de datos.

Todavía sugiero hacer el primer caso. Es una buena práctica para relaciones reales más tarde.


El cambio de ArrayList <String> a String con valores separados por comas funcionó para mí.
Chris Dale el

2
Pero esto lo obliga a usar declaraciones similares (feo) feas al consultar ese campo.
whiskeysierra

Sí, como dije ... haz la primera opción, es mejor. Si no puede hacerlo, la opción 2 puede funcionar.
billjamesdev

15

Según Java Persistence con Hibernate

mapeo de colecciones de tipos de valores con anotaciones [...]. Al momento de escribir, no es parte del estándar Java Persistence

Si estabas usando Hibernate, podrías hacer algo como:

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

Actualización: Nota, esto ahora está disponible en JPA2.


12

También podemos usar esto.

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

1
probablemente más @JoinTable.
phil294

10

Tuve el mismo problema, así que invertí la posible solución dada, pero al final decidí implementar mi ';' lista separada de cadenas.

así que tengo

// a ; separated list of arguments
String arguments;

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

De esta forma, la lista es fácilmente legible / editable en la tabla de la base de datos;


1
Esto es totalmente válido, pero considere el crecimiento de su aplicación y la evolución del esquema. En algún momento en el futuro (cercano), eventualmente podría cambiar al enfoque basado en la entidad.
whiskeysierra

Estoy de acuerdo, eso es totalmente válido. Sin embargo, sugiero revisar completamente la lógica y la implementación del código. Si String argumentses una lista de permisos de acceso, entonces tener un carácter especial, a separator, podría ser vulnerable a ataques de escalada de privilegios.
Thang Pham

1
Este es realmente un mal consejo, su cadena puede contener ;lo que romperá su aplicación.
agilob

9

Cuando uso la implementación Hibernate de JPA, descubrí que simplemente declarar el tipo como ArrayList en lugar de List permite que hibernate almacene la lista de datos.

Claramente, esto tiene una serie de desventajas en comparación con la creación de una lista de objetos de entidad. Sin carga diferida, sin capacidad de hacer referencia a las entidades en la lista desde otros objetos, tal vez más dificultad para construir consultas de bases de datos. Sin embargo, cuando se trata de listas de tipos bastante primitivos que siempre querrá buscar con entusiasmo junto con la entidad, este enfoque me parece bien.

@Entity
public class Command implements Serializable {

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

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


}

2
Gracias. Este trabajo con todas las implementaciones JPA, Arraylist is Serializable se guarda en un campo BLOB. Los problemas con este método son que 1) el tamaño BLOB es fijo 2) puede buscar o indexar los elementos de la matriz 3) solo un cliente que conozca el formato de serialización Java puede leer estos elementos.
Andrea Francia

En caso de que intente este enfoque @OneToMany @ManyToOne @ElementCollection, le dará una Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementsexcepción al iniciar el servidor. Porque hibernates quiere que uses interfaces de colección.
Paramvir Singh Karwal

9

Parece que ninguna de las respuestas exploró las configuraciones más importantes para un @ElementCollection mapeo.

Cuando asigna una lista con esta anotación y deja que JPA / Hibernate genere automáticamente las tablas, columnas, etc., también utilizará los nombres generados automáticamente.

Entonces, analicemos un ejemplo básico:

@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. La @ElementCollectionanotación básica (donde puede definir lo conocido fetchy las targetClasspreferencias)
  2. La @CollectionTableanotación es muy útil cuando se trata de dar un nombre a la tabla que va a ser generado, así como las definiciones como joinColumns, foreignKey's, indexes, uniqueConstraints, etc.
  3. @ColumnEs importante definir el nombre de la columna que almacenará el varcharvalor de la lista.

La creación de DDL generada sería:

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

44
Me gusta esta solución porque es la única solución propuesta que proporciona la descripción completa, incluidas las estructuras TABLE, y explica por qué necesitamos las diferentes anotaciones.
Julien Kronegg

6

Ok, sé que es un poco tarde. Pero para esas almas valientes que verán esto a medida que pase el tiempo.

Como está escrito en la documentación :

@Basic: el tipo más simple de asignación a una columna de base de datos. La anotación básica se puede aplicar a una propiedad persistente o variable de instancia de cualquiera de los siguientes tipos: tipos primitivos de Java, [...] enumeraciones y cualquier otro tipo que implemente java.io.Serializable.

La parte importante es el tipo que implementa Serializable

Entonces, con mucho, la solución más simple y fácil de usar es simplemente usar ArrayList en lugar de List (o cualquier contenedor serializable):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Sin embargo, recuerde que esto usará la serialización del sistema, por lo que tendrá un precio, como:

  • Si el modelo de objetos serializados cambia, es posible que no pueda restaurar los datos

  • Se agrega una pequeña sobrecarga para cada elemento almacenado.

En breve

es bastante simple almacenar banderas o pocos elementos, pero no lo recomendaría para almacenar datos que podrían crecer.


probé esto pero la tabla sql convirtió el tipo de datos en un bloque minúsculo. ¿No hace esto que insertar y recuperar la lista de cadenas sea muy inconveniente? ¿O jpa serializa y deserializa automáticamente para usted?
Dzhao

3

La respuesta de Thiago es correcta, agregando una muestra más específica a la pregunta, @ElementCollection creará una nueva tabla en su base de datos, pero sin asignar dos tablas, significa que la colección no es una colección de entidades, sino una colección de tipos simples (cadenas, etc. .) o una colección de elementos integrables (clase anotada con @Embeddable ).

Aquí está la muestra para persistir la lista de cadenas

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

Aquí está la muestra para mantener la lista de objetos personalizados

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

Para este caso, necesitamos hacer la clase Embeddable

@Embeddable
public class Car {
}

3

Aquí está la solución para almacenar un Set usando @Converter y StringTokenizer. Un poco más verifica la solución @ jonck-van-der-kogel .

En su clase de entidad:

@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

Mi solución para este problema fue separar la clave principal con la clave externa. Si está utilizando eclipse y realizó los cambios anteriores, recuerde actualizar el explorador de la base de datos. Luego, recrea las entidades de las tablas.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.