จะคงคุณสมบัติของชนิดรายการ <String> ใน JPA ได้อย่างไร


158

วิธีที่ฉลาดที่สุดในการรับเอนทิตีที่มีฟิลด์ชนิดรายการยังคงอยู่คืออะไร

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

รหัสนี้ผลิต:

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

คำตอบ:


197

ใช้การนำ JPA 2 ไปใช้: เพิ่มการเพิ่มความคิดเห็น @ElementCollection ซึ่งคล้ายกับการไฮเบอร์เนตที่ทำสิ่งที่คุณต้องการอย่างแท้จริง มีตัวอย่างหนึ่งคือที่นี่

แก้ไข

ดังที่กล่าวไว้ในความคิดเห็นด้านล่างการใช้งาน JPA 2 ที่ถูกต้องคือ

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

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


1
ความผิดพลาดของฉันคือการเพิ่มคำอธิบายประกอบ @ OneToMany เช่นกัน ... หลังจากลบออกและเพิ่งออกจาก @ ElementCollection มันใช้ได้
Willi Mentzel

47

ขออภัยที่จะฟื้นเธรดเก่า แต่ทุกคนควรมองหาโซลูชันทางเลือกที่คุณเก็บรายการสตริงเป็นหนึ่งฟิลด์ในฐานข้อมูลของคุณนี่คือวิธีที่ฉันแก้ไข สร้างตัวแปลงเช่นนี้:

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

ตอนนี้ใช้มันในเอนทิตี้ของคุณเช่นนี้:

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

ในฐานข้อมูลรายการของคุณจะถูกเก็บไว้เป็น foo; bar; foobar และในวัตถุ Java ของคุณคุณจะได้รับรายการที่มีสตริงเหล่านั้น

หวังว่านี่จะเป็นประโยชน์กับใครบางคน


มันจะทำงานกับที่เก็บ jpa เพื่อกรองผลลัพธ์ตามเนื้อหาของฟิลด์นั้นหรือไม่?
Please_Dont_Bully_Me_SO_Lords

1
@Please_Dont_Bully_Me_SO_Lords ไม่เหมาะสำหรับกรณีการใช้งานเนื่องจากข้อมูลของคุณจะอยู่ในฐานข้อมูลเป็น "foo; bar; foobar" หากคุณต้องการสืบค้นข้อมูลอาจเป็นเพราะ ElementCollection + JoinTable เป็นวิธีในการแก้ไขสถานการณ์ของคุณ
Jonck van der Kogel

นอกจากนี้ยังหมายความว่าคุณไม่สามารถSPLIT_CHARเกิดขึ้นในสายของคุณ
บดขยี้

@ crush ที่ถูกต้อง แม้ว่าแน่นอนว่าคุณสามารถอนุญาตได้โดยยกตัวอย่างเช่นการเข้ารหัสสตริงของคุณหลังจากที่คุณได้คั่นอย่างถูกต้อง แต่โซลูชันที่ฉันโพสต์ที่นี่มีไว้สำหรับกรณีการใช้งานที่เรียบง่ายเป็นหลัก สำหรับสถานการณ์ที่ซับซ้อนมากขึ้นคุณอาจรู้สึกดีขึ้นกับ ElementCollection + JoinTable
Jonck van der Kogel

โปรดแก้ไขรหัสของคุณ ฉันคิดว่ามันเป็น 'รหัสห้องสมุด' ดังนั้นจึงควรป้องกันเช่นอย่างน้อยก็ควรมีการตรวจสอบเป็นโมฆะ
ZZ 5

30

คำตอบนี้ถูกสร้างขึ้นก่อนการใช้งาน JPA2 หากคุณใช้ JPA2 ดูคำตอบ ElementCollection ด้านบน:

รายการของวัตถุภายในวัตถุรูปแบบโดยทั่วไปถือว่าเป็นความสัมพันธ์ "OneToMany" กับวัตถุอื่น อย่างไรก็ตามสตริงไม่ได้ (โดยตัวเอง) ไคลเอนต์ที่อนุญาตของความสัมพันธ์แบบหนึ่ง - ต่อ - กลุ่มเนื่องจากไม่มี ID

ดังนั้นคุณควรแปลงรายการ Strings ของคุณเป็นรายการของออบเจ็กต์ JPA ระดับอาร์กิวเมนต์ที่มี ID และสตริง คุณอาจใช้ String เป็น ID ซึ่งจะช่วยประหยัดพื้นที่เล็ก ๆ ในตารางของคุณทั้งจากการลบฟิลด์ ID และโดยการรวมแถวที่ Strings มีค่าเท่ากัน แต่คุณจะสูญเสียความสามารถในการเรียงลำดับอาร์กิวเมนต์กลับไปยังลำดับเดิม (เนื่องจากคุณไม่ได้เก็บข้อมูลการสั่งซื้อใด ๆ )

หรือคุณสามารถแปลงรายการของคุณเป็น @Transient และเพิ่มฟิลด์อื่น (argStorage) ให้กับคลาสของคุณซึ่งเป็น VARCHAR () หรือ CLOB จากนั้นคุณจะต้องเพิ่มฟังก์ชัน 3 ฟังก์ชัน: 2 ฟังก์ชันเหมือนกันและควรแปลงรายการ Strings ของคุณให้เป็นสตริงเดี่ยว (ใน argStorage) ที่คั่นด้วยรูปแบบที่คุณสามารถแยกได้อย่างง่ายดาย อธิบายฟังก์ชั่นทั้งสองนี้ (ที่แต่ละคนทำสิ่งเดียวกัน) ด้วย @PrePersist และ @PreUpdate ในที่สุดเพิ่มฟังก์ชั่นที่สามที่แยก argStorage ลงในรายการ Strings อีกครั้งและใส่คำอธิบายประกอบ @PostLoad สิ่งนี้จะทำให้ CLOB ของคุณอัปเดตด้วยสตริงทุกครั้งที่คุณไปเก็บคำสั่งและเก็บฟิลด์ argStorage ที่อัพเดตก่อนที่คุณจะเก็บมันไว้ในฐานข้อมูล

ฉันยังคงแนะนำให้ทำกรณีแรก เป็นการปฏิบัติที่ดีสำหรับความสัมพันธ์ที่แท้จริงในภายหลัง


การเปลี่ยนจาก ArrayList <String> เป็น String ด้วยค่าที่คั่นด้วยเครื่องหมายจุลภาคเหมาะสำหรับฉัน
คริสเดล

2
แต่สิ่งนี้บังคับให้คุณใช้ (imho) เหมือนคำสั่งที่น่าเกลียดเมื่อทำการค้นหาในฟิลด์นั้น
whiskinssierra

ใช่อย่างที่ฉันบอก ... ทำสิ่งแรกให้ดีกว่า หากคุณไม่สามารถพาตัวเองไปทำมันได้ตัวเลือกที่ 2 สามารถใช้งานได้
billjamesdev

15

อ้างอิงจากJava Persistence with Hibernate

การแม็พคอลเลกชันของประเภทค่าที่มีหมายเหตุประกอบ [... ] ในขณะที่เขียนมันไม่ได้เป็นส่วนหนึ่งของมาตรฐานความคงทนของ Java

หากคุณใช้ Hibernate คุณสามารถทำสิ่งต่อไปนี้:

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

อัปเดต: โปรดทราบตอนนี้มีให้บริการใน JPA2



10

ฉันมีปัญหาเดียวกันดังนั้นฉันจึงลงทุนวิธีแก้ปัญหาที่เป็นไปได้ แต่ในที่สุดฉันก็ตัดสินใจใช้ ';' รายการแยกของ String

ดังนั้นฉันมี

// a ; separated list of arguments
String arguments;

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

วิธีนี้รายการสามารถอ่าน / แก้ไขได้ง่ายในตารางฐานข้อมูล


1
สิ่งนี้ใช้ได้อย่างสมบูรณ์ แต่พิจารณาการเติบโตของแอปพลิเคชันของคุณและวิวัฒนาการของสคีมา บางครั้งในอนาคต (ใกล้) คุณอาจเปลี่ยนไปใช้วิธีการตามเอนทิตี
whiskinssierra

ฉันยอมรับว่าถูกต้องทั้งหมด อย่างไรก็ตามฉันขอแนะนำให้ตรวจสอบตรรกะและการใช้รหัสอย่างสมบูรณ์ หาก String argumentsเป็นรายการสิทธิ์การเข้าถึงดังนั้นการมีอักขระพิเศษseparatorอาจจะเสี่ยงต่อการถูกโจมตีแบบเลื่อนระดับ
Thang Pham

1
นี่เป็นคำแนะนำที่ไม่ดีจริง ๆ สตริงของคุณอาจมี;แอพที่แตก
agilob

9

เมื่อใช้การใช้งาน Hibernate ของ JPA ฉันพบว่าการประกาศประเภทเป็น ArrayList แทนที่จะเป็น List ช่วยให้ Hibernate สามารถจัดเก็บรายการข้อมูลได้

เห็นได้ชัดว่านี่มีข้อเสียจำนวนหนึ่งเมื่อเทียบกับการสร้างรายการของวัตถุ Entity ไม่มีการโหลดที่ขี้เกียจไม่มีความสามารถในการอ้างอิงเอนทิตีในรายการจากวัตถุอื่น ๆ อาจจะยากในการสร้างคิวรีฐานข้อมูล อย่างไรก็ตามเมื่อคุณกำลังจัดการกับรายการประเภทดั้งเดิมค่อนข้างที่คุณจะต้องการดึงพร้อมกับเอนทิตี้เสมอแล้ววิธีการนี้ดูเหมือนจะดีสำหรับฉัน

@Entity
public class Command implements Serializable {

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

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


}

2
ขอบคุณ การทำงานกับการใช้งาน JPA ทั้งหมดนี้ Arraylist เป็น Serializable จะถูกบันทึกในฟิลด์ BLOB ปัญหาเกี่ยวกับวิธีนี้คือ 1) ขนาด BLOB ได้รับการแก้ไข 2) คุณสามารถค้นหาหรือจัดทำดัชนีองค์ประกอบอาร์เรย์ 3) เฉพาะลูกค้าที่ทราบเกี่ยวกับรูปแบบการทำให้เป็นอันดับ Java สามารถอ่านองค์ประกอบเหล่านี้ได้
Andrea Francia

ในกรณีที่คุณลองวิธีนี้ด้วย@OneToMany @ManyToOne @ElementCollectionจะให้Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElementsข้อยกเว้นในการเริ่มต้นเซิร์ฟเวอร์ เพราะไฮเบอร์เนตต้องการให้คุณใช้อินเทอร์เฟซการรวบรวม
Paramvir Singh Karwal

9

ดูเหมือนว่าไม่มีคำตอบใดที่สำรวจการตั้งค่าที่สำคัญที่สุดสำหรับการ@ElementCollectionทำแผนที่

เมื่อคุณจับคู่รายการด้วยหมายเหตุนี้และให้ JPA / Hibernate auto สร้างตารางคอลัมน์ ฯลฯ มันจะใช้ชื่อที่สร้างขึ้นอัตโนมัติเช่นกัน

ดังนั้นเรามาวิเคราะห์ตัวอย่างพื้นฐาน:

@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. @ElementCollectionคำอธิบายประกอบพื้นฐาน(ที่คุณสามารถกำหนดที่รู้จักfetchและtargetClassการตั้งค่า)
  2. @CollectionTableคำอธิบายประกอบเป็นประโยชน์อย่างมากเมื่อมันมาถึงชื่อให้กับตารางที่จะเกิดขึ้นเช่นเดียวกับคำนิยามเช่นjoinColumns, foreignKey's, indexes, uniqueConstraintsฯลฯ
  3. @Columnเป็นสิ่งสำคัญในการกำหนดชื่อของคอลัมน์ที่จะเก็บvarcharค่าของรายการ

การสร้าง DDL ที่สร้างขึ้นจะเป็น:

-- 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
ฉันชอบโซลูชันนี้เพราะเป็นโซลูชันเดียวที่เสนอซึ่งให้คำอธิบายแบบเต็มรวมถึงโครงสร้างของตารางและอธิบายว่าทำไมเราจึงต้องการคำอธิบายประกอบที่แตกต่างกัน
Julien Kronegg

6

ตกลงฉันรู้ว่ามันสายไปหน่อย แต่สำหรับวิญญาณผู้กล้าที่จะเห็นสิ่งนี้เมื่อเวลาผ่านไป

ตามที่เขียนไว้ในเอกสาร :

@Basic: การแมปประเภทที่ง่ายที่สุดไปยังคอลัมน์ฐานข้อมูล หมายเหตุประกอบพื้นฐานสามารถนำไปใช้กับคุณสมบัติถาวรหรือตัวแปรอินสแตนซ์ของชนิดต่อไปนี้: ประเภท Java ดั้งเดิม, [... ], enums และประเภทอื่น ๆ ที่ใช้ java.io.Serializable

ส่วนที่สำคัญคือประเภทที่ใช้ Serializable

ดังนั้นในที่สุดวิธีที่ง่ายที่สุดและง่ายที่สุดในการใช้งานก็คือการใช้ ArrayList แทน List (หรือคอนเทนเนอร์แบบอนุกรมใด ๆ ):

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

โปรดจำไว้ว่าสิ่งนี้จะใช้การทำให้เป็นอนุกรมของระบบดังนั้นมันจะมาพร้อมกับราคาบางอย่างเช่น:

  • หากโมเดลวัตถุที่เป็นอนุกรมจะเปลี่ยนแปลงคุณอาจไม่สามารถกู้คืนข้อมูลได้

  • มีการเพิ่มโสหุ้ยเล็กน้อยสำหรับแต่ละองค์ประกอบที่เก็บ

ในระยะสั้น

มันค่อนข้างง่ายในการจัดเก็บค่าสถานะหรือองค์ประกอบบางอย่าง แต่ฉันจะไม่แนะนำให้เก็บข้อมูลที่อาจเติบโตใหญ่


ลองนี้ แต่ตาราง sql ทำให้ประเภทข้อมูลเป็น tinyblob สิ่งนี้ทำให้การแทรกและดึงรายการสตริงไม่สะดวกอย่างยิ่งหรือไม่? jpa หรือทำให้เป็นอันดับและทำให้เป็นอันดับสำหรับคุณโดยอัตโนมัติหรือไม่
Dzhao

3

คำตอบ Thiago ถูกต้องการเพิ่มตัวอย่างเฉพาะคำถาม@ElementCollectionจะสร้างตารางใหม่ในฐานข้อมูลของคุณ แต่ไม่มีการแมปสองตารางหมายความว่าการรวบรวมไม่ใช่คอลเลกชันของเอนทิตี แต่เป็นการรวบรวมประเภทธรรมดา (สตริง ฯลฯ .) หรือคอลเล็กชันขององค์ประกอบที่ฝังได้ (คลาสที่ใส่คำอธิบายประกอบด้วย@Embeddable )

นี่คือตัวอย่างเพื่อยืนยันรายการของString

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

นี่คือตัวอย่างเพื่อยืนยันรายการของวัตถุที่กำหนดเอง

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

สำหรับกรณีนี้เราต้องทำให้คลาสเป็นแบบฝังได้

@Embeddable
public class Car {
}

3

นี่คือทางออกสำหรับการจัดเก็บชุดโดยใช้ @Converter และ StringTokenizer บิตการตรวจสอบมากขึ้นกับ@ jonck รถตู้-der-kogelวิธีการแก้ปัญหา

ในชั้นเรียนของคุณ:

@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

การแก้ไขของฉันสำหรับปัญหานี้คือการแยกคีย์หลักด้วยรหัสต่างประเทศ หากคุณกำลังใช้ eclipse และทำการเปลี่ยนแปลงข้างต้นโปรดอย่าลืมรีเฟรช explorer ฐานข้อมูล จากนั้นสร้างเอนทิตีจากตาราง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.