Bagaimana cara bertahan properti tipe Daftar <String> di JPA?

158

Apa cara terbaik untuk mendapatkan entitas dengan bidang tipe Daftar tetap?

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

Kode ini menghasilkan:

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

Jawaban:

197

Gunakan beberapa implementasi JPA 2: ia menambahkan anotasi @ElementCollection, mirip dengan yang Hibernate, yang melakukan persis apa yang Anda butuhkan. Ada satu contoh di sini .

Edit

Seperti disebutkan dalam komentar di bawah, implementasi JPA 2 yang benar adalah

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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

Thiago H. de Paula Figueiredo
sumber
1
Kesalahan saya adalah menambahkan @ OneToMany anotasi juga ... setelah menghapusnya dan meninggalkan @ ElementCollection berhasil
Willi Mentzel
47

Maaf untuk menghidupkan kembali utas lama tetapi jika ada yang mencari solusi alternatif tempat Anda menyimpan daftar string Anda sebagai satu bidang dalam basis data Anda, inilah cara saya menyelesaikannya. Buat Konverter seperti ini:

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

Sekarang gunakan di Entitas Anda seperti ini:

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

Dalam database daftar Anda akan disimpan sebagai foo; bar; foobar dan di objek Java Anda, Anda akan mendapatkan daftar dengan string tersebut.

Semoga ini bermanfaat bagi seseorang.

Jonck van der Kogel
sumber
Apakah ini akan bekerja dengan repositori jpa untuk memfilter hasil berdasarkan konten bidang itu?
Please_Dont_Bully_Me_SO_Lords
1
@Please_Dont_Bully_Me_SO_Lords Kurang cocok untuk kasus penggunaan karena data Anda akan ada di database sebagai "foo; bar; foobar". Jika Anda ingin meminta data maka mungkin ElementCollection + JoinTable adalah cara yang sesuai untuk situasi Anda.
Jonck van der Kogel
Ini juga berarti bahwa Anda tidak dapat memiliki SPLIT_CHARkejadian di string Anda.
naksir
@ Crush itu benar. Meskipun tentu saja Anda dapat mengizinkannya dengan misalnya menyandikan string Anda setelah Anda benar-benar membatasi itu. Tetapi solusi yang saya posting di sini terutama dimaksudkan untuk kasus penggunaan sederhana; untuk situasi yang lebih kompleks, mungkin Anda akan mendapatkan yang lebih baik dengan ElementCollection + JoinTable
Jonck van der Kogel
Harap perbaiki kode Anda. Saya menganggapnya sebagai 'kode perpustakaan' jadi harus defensif misalnya setidaknya harus memiliki pemeriksaan nol
ZZ 5
30

Jawaban ini dibuat sebelum implementasi JPA2, jika Anda menggunakan JPA2, lihat jawaban ElementCollection di atas:

Daftar objek di dalam objek model umumnya dianggap hubungan "OneToMany" dengan objek lain. Namun, sebuah String bukan (dengan sendirinya) klien yang diijinkan dari hubungan Satu-ke-Banyak, karena tidak memiliki ID.

Jadi, Anda harus mengonversi daftar Strings Anda ke daftar objek JPA Argument-class yang berisi ID dan String. Anda bisa berpotensi menggunakan String sebagai ID, yang akan menghemat sedikit ruang di tabel Anda baik dari menghapus bidang ID dan dengan mengkonsolidasikan baris di mana String sama, tetapi Anda akan kehilangan kemampuan untuk memesan argumen kembali ke urutan aslinya (karena Anda tidak menyimpan informasi pemesanan)

Atau, Anda bisa mengonversi daftar Anda ke @Transient dan menambahkan bidang lain (argStorage) ke kelas Anda yang merupakan VARCHAR () atau CLOB. Anda kemudian perlu menambahkan 3 fungsi: 2 di antaranya adalah sama dan harus mengubah daftar Strings Anda menjadi satu String (dalam argStorage) dibatasi dengan cara yang Anda dapat dengan mudah memisahkannya. Beri catatan dua fungsi ini (yang masing-masing melakukan hal yang sama) dengan @PrePersist dan @PreUpdate. Terakhir, tambahkan fungsi ketiga yang membagi argStorage ke dalam daftar Strings lagi dan beri catatan @PostLoad. Ini akan membuat CLOB Anda diperbarui dengan string kapan pun Anda pergi untuk menyimpan Perintah, dan menjaga bidang argStorage diperbarui sebelum Anda menyimpannya ke DB.

Saya masih menyarankan melakukan kasus pertama. Ini praktik yang baik untuk hubungan yang nyata nanti.

billjamesdev
sumber
Mengubah dari ArrayList <String> ke String dengan nilai dipisahkan koma bekerja untuk saya.
Chris Dale
2
Tapi ini memaksa Anda untuk menggunakan (suka) pernyataan seperti jelek saat menanyakan bidang itu.
whiskeysierra
Ya, seperti yang saya katakan ... lakukan opsi pertama, lebih baik. Jika Anda tidak dapat melakukannya, opsi 2 dapat digunakan.
billjamesdev
15

Menurut Java Persistence with Hibernate

memetakan koleksi tipe nilai dengan anotasi [...]. Pada saat penulisan ini bukan bagian dari standar Java Persistence

Jika Anda menggunakan Hibernate, Anda bisa melakukan sesuatu seperti:

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

Pembaruan: Catatan, ini sekarang tersedia di JPA2.

toolkit
sumber
12

Kita juga bisa menggunakan ini.

@Column(name="arguments")
@ElementCollection(targetClass=String.class)
private List<String> arguments;
Jaimin Patel
sumber
1
mungkin ditambah @JoinTable.
phil294
10

Saya memiliki masalah yang sama sehingga saya menginvestasikan solusi yang mungkin diberikan tetapi pada akhirnya saya memutuskan untuk menerapkan ';' daftar String yang terpisah.

jadi saya punya

// a ; separated list of arguments
String arguments;

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

Dengan cara ini daftar ini mudah dibaca / diedit dalam tabel database;

Anthony
sumber
1
Ini benar-benar valid tetapi pertimbangkan pertumbuhan aplikasi Anda dan evolusi skema. Suatu saat di masa depan (dekat) Anda mungkin akhirnya beralih ke pendekatan berbasis entitas.
whiskeysierra
Saya setuju, itu benar-benar valid. Namun, saya menyarankan untuk sepenuhnya meninjau logika serta implementasi kode. Jika String argumentsadalah daftar izin akses, maka memiliki karakter khusus, a separator, mungkin rentan terhadap serangan eskalasi hak istimewa.
Thang Pham
1
Ini saran yang sangat buruk, string Anda mungkin berisi ;yang akan merusak aplikasi Anda.
agilob
9

Saat menggunakan implementasi Hibernate JPA, saya telah menemukan bahwa dengan mendeklarasikan tipe sebagai ArrayList, bukan List, memungkinkan hibernate untuk menyimpan daftar data.

Jelas ini memiliki sejumlah kelemahan dibandingkan dengan membuat daftar objek Entitas. Tidak ada pemuatan malas, tidak ada kemampuan untuk referensi entitas dalam daftar dari objek lain, mungkin lebih sulit dalam membangun query database. Namun ketika Anda berhadapan dengan daftar jenis yang cukup primitif yang Anda selalu ingin bersemangat untuk mengambil bersama dengan entitas, maka pendekatan ini tampaknya baik untuk saya.

@Entity
public class Command implements Serializable {

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

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


}

sumber
2
Terima kasih. Ini bekerja dengan semua implementasi JPA, Arraylist is Serializable disimpan di bidang BLOB. Masalah dengan metode ini adalah 1) ukuran BLOB diperbaiki 2) Anda dapat mencari atau mengindeks elemen array 3) hanya klien yang mengetahui tentang format serialisasi Java yang dapat membaca elemen-elemen ini.
Andrea Francia
Jika Anda mencoba pendekatan ini dengan @OneToMany @ManyToOne @ElementCollectionitu akan memberi Anda Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementspengecualian pada startup server. Karena hibernasi ingin Anda menggunakan antarmuka koleksi.
Paramvir Singh Karwal
9

Tampaknya tidak ada jawaban yang mengeksplorasi pengaturan paling penting untuk @ElementCollectionpemetaan.

Saat Anda memetakan daftar dengan anotasi ini, dan biarkan JPA / Hibernate otomatis membuat tabel, kolom, dll., Ia juga akan menggunakan nama yang dihasilkan secara otomatis.

Jadi, mari kita menganalisis contoh dasar:

@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. @ElementCollectionAnotasi dasar (di mana Anda dapat menentukan yang diketahui fetchdan targetClasspreferensi)
  2. The @CollectionTablepenjelasan ini sangat berguna ketika datang untuk memberikan nama untuk tabel yang akan dihasilkan, serta definisi seperti joinColumns, foreignKey's, indexes, uniqueConstraints, dll
  3. @ColumnPenting untuk menentukan nama kolom yang akan menyimpan varcharnilai daftar.

Pembuatan DDL yang dihasilkan adalah:

-- 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
sumber
4
Saya suka solusi ini karena ini adalah satu-satunya solusi yang diusulkan yang memberikan deskripsi lengkap termasuk struktur TABEL dan menjelaskan mengapa kita memerlukan anotasi yang berbeda.
Julien Kronegg
6

Ok saya tahu agak terlambat. Tetapi bagi jiwa-jiwa pemberani yang akan melihat ini seiring berjalannya waktu.

Seperti yang tertulis dalam dokumentasi :

@Basic: Jenis pemetaan paling sederhana ke kolom basis data. Anotasi dasar dapat diterapkan ke properti persisten atau variabel instan dari salah satu dari tipe berikut: tipe primitif Java, [...], enum, dan tipe lain apa pun yang mengimplementasikan java.io.Serializable.

Bagian yang penting adalah tipe yang mengimplementasikan Serializable

Jadi sejauh ini solusi yang paling sederhana dan termudah untuk digunakan adalah hanya menggunakan ArrayList, bukan List (atau wadah serializable):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

Namun perlu diingat bahwa ini akan menggunakan sistem serialisasi, sehingga akan datang dengan beberapa harga, seperti:

  • jika model objek serial akan berubah, Anda mungkin tidak dapat mengembalikan data

  • overhead kecil ditambahkan untuk setiap elemen yang disimpan.

Pendeknya

cukup sederhana untuk menyimpan flag atau beberapa elemen, tetapi saya tidak akan merekomendasikannya untuk menyimpan data yang mungkin bertambah besar.

Inverce
sumber
mencoba ini tetapi tabel sql membuat datatype blok kecil. Tidakkah ini membuat menyisipkan dan mengambil daftar string sangat tidak nyaman? Atau apakah jpa secara otomatis membuat serial dan deserialize untuk Anda?
Dzhao
3

Jawaban thiago benar, menambahkan sampel yang lebih spesifik untuk pertanyaan, @ElementCollection akan membuat tabel baru di database Anda, tetapi tanpa memetakan dua tabel, itu berarti bahwa koleksi tersebut bukan kumpulan entitas, tetapi kumpulan tipe sederhana (String, dll. .) atau kumpulan elemen yang dapat di- embed (kelas dijelaskan dengan @Embeddable ).

Berikut adalah contoh untuk mempertahankan daftar String

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

Ini adalah contoh untuk melanjutkan daftar objek Kustom

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

Untuk hal ini kita perlu membuat kelas Embeddable

@Embeddable
public class Car {
}
Zia
sumber
3

Ini adalah solusi untuk menyimpan Set menggunakan @Converter dan StringTokenizer. Pemeriksaan lebih sedikit terhadap solusi @ jonck-van-der-kogel .

Di kelas Entity Anda:

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

Perbaikan saya untuk masalah ini adalah memisahkan kunci utama dengan kunci asing. Jika Anda menggunakan eclipse dan melakukan perubahan di atas, harap ingat untuk menyegarkan basis data explorer. Kemudian buat kembali entitas dari tabel.

kekuatan daging
sumber