Como persistir uma propriedade do tipo List <> no JPA?

158

Qual é a maneira mais inteligente de manter uma entidade com um campo do tipo Lista?

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

> 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
fonte

Respostas:

197

Use alguma implementação JPA 2: ela adiciona uma anotação @ElementCollection, semelhante à do Hibernate, que faz exatamente o que você precisa. Há um exemplo aqui .

Editar

Conforme mencionado nos comentários abaixo, a implementação correta da JPA 2 é

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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

Thiago H. de Paula Figueiredo
fonte
1
Meu erro foi adicionar @ OneToMany anotação, bem ... depois de removê-lo e apenas deixando @ ElementCollection funcionou
Willi Mentzel
46

Desculpe reviver um thread antigo, mas se alguém estiver procurando por uma solução alternativa em que você armazene suas listas de strings como um campo no banco de dados, eis como eu resolvi isso. Crie um conversor 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));
    }
}

Agora use-o em suas entidades como esta:

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

No banco de dados, sua lista será armazenada como foo; bar; foobar e no seu objeto Java, você obterá uma lista com essas strings.

Espero que isto seja útil para alguém.

Jonck van der Kogel
fonte
Funcionará com repositórios jpa para filtrar os resultados pelo conteúdo desse campo?
Please_Dont_Bully_Me_SO_Lords
1
@Please_Dont_Bully_Me_SO_Lords É menos adequado para esse caso de uso, pois seus dados estarão no banco de dados como "foo; bar; foobar". Se você deseja consultar os dados, provavelmente uma ElementCollection + JoinTable é o caminho a seguir para sua situação.
Jonck van der Kogel 02/03/19
Isso também significa que você não pode ter SPLIT_CHARocorrências na sua string.
esmagar
@crush está correto. Embora, é claro, você possa permitir isso, por exemplo, codificando sua string depois de a delimitar corretamente. Mas a solução que publiquei aqui se destina principalmente a casos de uso simples; para situações mais complexas, provavelmente você vai se sair melhor com um ElementCollection + JoinTable
Jonck van der Kogel
Corrija seu código. Eu o considero como 'código de biblioteca', portanto, deve ser defensivo, por exemplo, pelo menos, deve ter verificações nulas
ZZ
30

Esta resposta foi feita em implementações anteriores ao JPA2, se você estiver usando o JPA2, consulte a resposta do ElementCollection acima:

As listas de objetos dentro de um objeto de modelo são geralmente consideradas relacionamentos "OneToMany" com outro objeto. No entanto, uma String não é (por si só) um cliente permitido de um relacionamento um para muitos, pois não possui um ID.

Portanto, você deve converter sua lista de Strings em uma lista de objetos JPA da classe Argument que contêm um ID e uma String. Você poderia usar a String como o ID, o que economizaria um pouco de espaço na tabela, removendo o campo ID e consolidando as linhas onde as Strings são iguais, mas você perderia a capacidade de ordenar os argumentos de volta à ordem original. (como você não armazenou nenhuma informação de pedido).

Como alternativa, você pode converter sua lista em @Transient e adicionar outro campo (argStorage) à sua classe que é um VARCHAR () ou um CLOB. Você precisará adicionar três funções: duas delas são iguais e deve converter sua lista de Strings em uma única String (em argStorage) delimitada de uma maneira que você possa separá-las facilmente. Anote essas duas funções (que cada uma faz a mesma coisa) com @PrePersist e @PreUpdate. Por fim, adicione a terceira função que divide o argStorage na lista de Strings novamente e anote-o @PostLoad. Isso manterá seu CLOB atualizado com as seqüências sempre que você for armazenar o comando e o campo argStorage atualizado antes de armazená-lo no banco de dados.

Eu ainda sugiro fazer o primeiro caso. É uma boa prática para relacionamentos reais mais tarde.

billjamesdev
fonte
Mudar de ArrayList <> para String com valores separados por vírgula funcionou para mim.
21420 Chris Dale
2
Mas isso força você a usar (imho) instruções semelhantes feias ao consultar esse campo.
precisa
Sim, como eu disse ... faça a primeira opção, é melhor. Se você simplesmente não consegue fazê-lo, a opção 2 pode funcionar.
billjamesdev
15

De acordo com a persistência de Java com o Hibernate

mapeamento de coleções de tipos de valor com anotações [...]. No momento da redação, ele não faz parte do padrão Java Persistence

Se você estivesse usando o Hibernate, poderia fazer 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>();

Atualização: observe que agora está disponível no JPA2.

kit de ferramentas
fonte
12

Nós também podemos usar isso.

@Column(name="arguments")
@ElementCollection(targetClass=String.class)
private List<String> arguments;
Jaimin Patel
fonte
1
provavelmente mais @JoinTable.
phil294
9

Ao usar a implementação do JPA no Hibernate, descobri que simplesmente declarar o tipo como ArrayList em vez de List permite que o hibernate armazene a lista de dados.

Claramente, isso tem várias desvantagens em comparação à criação de uma lista de objetos de entidade. Sem carregamento lento, sem capacidade de referenciar as entidades na lista de outros objetos, talvez com mais dificuldade na construção de consultas ao banco de dados. No entanto, quando você está lidando com listas de tipos bastante primitivos que você sempre desejará buscar junto com a entidade, essa abordagem me parece adequada.

@Entity
public class Command implements Serializable {

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

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


}

fonte
2
Obrigado. Este trabalho com todas as implementações JPA, Arraylist is Serizable, é salvo em um campo BLOB. Os problemas com esse método são: 1) o tamanho do BLOB é corrigido 2) é possível pesquisar ou indexar os elementos da matriz 3) apenas um cliente ciente do formato de serialização Java pode ler esses elementos.
Andrea Francia
Caso você tente essa abordagem, @OneToMany @ManyToOne @ElementCollectionela abrirá uma Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementsexceção na inicialização do servidor. Porque o hibernates quer que você use interfaces de coleção.
Paramvir Singh Karwal 23/03/19
9

Eu tinha o mesmo problema, então investi na possível solução, mas no final decidi implementar meu ';' lista separada de String.

então eu tenho

// a ; separated list of arguments
String arguments;

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

Dessa forma, a lista é facilmente legível / editável na tabela do banco de dados;

Anthony
fonte
1
Isso é totalmente válido, mas considere o crescimento do seu aplicativo e a evolução do esquema. Em algum momento no (próximo) futuro, você poderá eventualmente mudar para a abordagem baseada na entidade.
precisa
Eu concordo, isso é totalmente válido. No entanto, sugiro revisar completamente a lógica e a implementação do código. Se String argumentsfor uma lista de permissões de acesso, o caractere especial a separatorpoderá ser vulnerável a ataques de escalação de privilégios.
Thang Pham
1
Este é um péssimo conselho, sua string pode conter o ;que interromperá seu aplicativo.
agilob
9

Parece que nenhuma das respostas explorou as configurações mais importantes para um @ElementCollectionmapeamento.

Quando você mapeia uma lista com esta anotação e permite que o JPA / Hibernate gere automaticamente as tabelas, colunas, etc., ele também usará nomes gerados automaticamente.

Então, vamos analisar um exemplo 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. A @ElementCollectionanotação básica (onde você pode definir o conhecido fetche as targetClasspreferências)
  2. A @CollectionTableanotação é muito útil quando se trata de dar um nome para a mesa que vai ser gerado, bem como definições como joinColumns, foreignKey's, indexes, uniqueConstraints, etc.
  3. @Columné importante definir o nome da coluna que armazenará o varcharvalor da lista.

A criação DDL gerada seria:

-- 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
fonte
4
Eu gosto dessa solução porque é a única solução proposta que fornece a descrição completa, incluindo as estruturas TABLE e explica por que precisamos das diferentes anotações.
Julien Kronegg 24/01
6

Ok, eu sei que é um pouco tarde. Mas para aquelas almas corajosas que verão isso com o passar do tempo.

Conforme escrito na documentação :

@ Basic: O tipo mais simples de mapeamento para uma coluna do banco de dados. A anotação básica pode ser aplicada a uma propriedade persistente ou variável de instância de qualquer um dos seguintes tipos: tipos primitivos Java, [...] enumerações e qualquer outro tipo que implemente java.io.Serializable.

A parte importante é o tipo que implementa Serializable

Portanto, de longe, a solução mais simples e fácil de usar é simplesmente usar ArrayList em vez de List (ou qualquer contêiner serializável):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Lembre-se, no entanto, de que isso usará a serialização do sistema e, portanto, terá algum preço, como:

  • se o modelo de objeto serializado mudar, talvez você não consiga restaurar dados

  • uma pequena sobrecarga é adicionada para cada elemento armazenado.

Em resumo

é bastante simples armazenar sinalizadores ou poucos elementos, mas eu não o recomendaria para armazenar dados que podem crescer muito.

Inverce
fonte
tentei isso, mas a tabela sql transformou o tipo de dados em um pequeno blob. Isso não torna muito complicado inserir e recuperar a lista de seqüências de caracteres? Ou o jpa serializa e desserializa automaticamente para você?
Dzhao
3

A resposta de Thiago está correta, adicionando amostra mais específica à pergunta, o @ElementCollection criará uma nova tabela no seu banco de dados, mas sem mapear duas tabelas, significa que a coleção não é uma coleção de entidades, mas uma coleção de tipos simples (Strings, etc. .) ou uma coleção de elementos incorporáveis ​​(classe anotada com @Embeddable ).

Aqui está o exemplo para persistir a lista de String

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

Aqui está o exemplo para persistir a lista de objetos Personalizados

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

Nesse caso, precisamos tornar a classe Embeddable

@Embeddable
public class Car {
}
Zia
fonte
3

Aqui está a solução para armazenar um conjunto usando o @Converter e o StringTokenizer. Um pouco mais de verificação na solução @ jonck-van-der-kogel .

Na sua classe de entidade:

@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
fonte
1

Minha correção para esse problema foi separar a chave primária pela chave estrangeira. Se você estiver usando o eclipse e fez as alterações acima, lembre-se de atualizar o explorador de banco de dados. Em seguida, recrie as entidades das tabelas.

poderes carne
fonte