วิธีแมปคีย์ผสมกับ JPA และไฮเบอร์เนต


204

ในรหัสนี้วิธีสร้างคลาส Java สำหรับคอมโพสิตคีย์ (วิธีผสมคีย์ในไฮเบอร์เนต):

create table Time (
     levelStation int(15) not null,
     src varchar(100) not null,
     dst varchar(100) not null,
     distance int(15) not null,
     price int(15) not null,
     confPathID int(15) not null,
     constraint ConfPath_fk foreign key(confPathID) references ConfPath(confPathID),
     primary key (levelStation, confPathID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


1
ชุดตัวอย่างที่ดีจริงๆ: vladmihalcea.com/2016/08/01/…
TecHunter

คำตอบ:


415

ต้องการแมคีย์คอมโพสิตคุณสามารถใช้EmbeddedId หรือIdClassคำอธิบายประกอบ ฉันรู้ว่าคำถามนี้ไม่ได้เกี่ยวกับ JPA อย่างเคร่งครัด แต่กฎที่กำหนดโดยข้อกำหนดยังใช้ ดังนั้นพวกเขาอยู่ที่นี่:

2.1.4 คีย์หลักและรหัสประจำตัว

...

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

...

กฎต่อไปนี้ใช้กับคีย์หลักแบบรวม:

  • คลาสคีย์หลักต้องเป็นสาธารณะและต้องมีตัวสร้างแบบไม่มีอาร์กิวเมนต์สาธารณะ
  • หากใช้การเข้าถึงตามคุณสมบัติคุณสมบัติของคลาสคีย์หลักต้องเป็นพับลิกหรือได้รับการป้องกัน
  • serializableชั้นคีย์หลักจะต้องเป็น
  • คลาสคีย์หลักต้องกำหนดequalsและhashCode วิธีการ ความหมายของความเท่าเทียมกันของค่าสำหรับวิธีการเหล่านี้จะต้องสอดคล้องกับความเท่าเทียมกันของฐานข้อมูลสำหรับประเภทฐานข้อมูลที่คีย์ถูกแมป
  • คีย์หลักแบบผสมต้องแสดงและแมปเป็นคลาสที่สามารถฝังได้ (ดูส่วน 9.1.14, "คำอธิบายประกอบแบบฝังตัว") หรือต้องแสดงและแมปกับหลาย ๆ ฟิลด์หรือคุณสมบัติของคลาสเอนทิตี (ดูหัวข้อ 9.1.15, "IdClass คำอธิบายประกอบ”)
  • ถ้าคลาสคีย์หลักผสมถูกแมปไปยังหลายเขตข้อมูลหรือคุณสมบัติของคลาสเอนทิตี้ชื่อของเขตข้อมูลคีย์หลักหรือคุณสมบัติในคลาสคีย์หลักและของคลาสเอนทิตีต้องตรงกันและประเภทของพวกเขาจะต้องเหมือนกัน

ด้วยการ IdClass

คลาสสำหรับคีย์หลักคอมโพสิตอาจมีลักษณะ (อาจเป็นคลาสภายในคงที่):

public class TimePK implements Serializable {
    protected Integer levelStation;
    protected Integer confPathID;

    public TimePK() {}

    public TimePK(Integer levelStation, Integer confPathID) {
        this.levelStation = levelStation;
        this.confPathID = confPathID;
    }
    // equals, hashCode
}

และหน่วยงาน:

@Entity
@IdClass(TimePK.class)
class Time implements Serializable {
    @Id
    private Integer levelStation;
    @Id
    private Integer confPathID;

    private String src;
    private String dst;
    private Integer distance;
    private Integer price;

    // getters, setters
}

IdClassคำอธิบายประกอบแผนที่หลายเขตข้อมูลไปยังตาราง PK

กับ EmbeddedId

คลาสสำหรับคีย์หลักคอมโพสิตอาจมีลักษณะ (อาจเป็นคลาสภายในคงที่):

@Embeddable
public class TimePK implements Serializable {
    protected Integer levelStation;
    protected Integer confPathID;

    public TimePK() {}

    public TimePK(Integer levelStation, Integer confPathID) {
        this.levelStation = levelStation;
        this.confPathID = confPathID;
    }
    // equals, hashCode
}

และหน่วยงาน:

@Entity
class Time implements Serializable {
    @EmbeddedId
    private TimePK timePK;

    private String src;
    private String dst;
    private Integer distance;
    private Integer price;

    //...
}

@EmbeddedIdคำอธิบายประกอบแผนที่ระดับ PK เพื่อ PK ตาราง

แตกต่าง:

  • จากมุมมองแบบจำลองทางกายภาพไม่มีความแตกต่าง
  • @EmbeddedIdอย่างใดติดต่อสื่อสารได้อย่างชัดเจนมากขึ้นที่สำคัญคือคีย์คอมโพสิตและ IMO ทำให้รู้สึกเมื่อ PK รวมเป็นทั้งนิติบุคคลที่มีความหมายของตัวเองหรือนำกลับมาใช้ในรหัสของคุณ
  • @IdClass เป็นประโยชน์ในการระบุว่าการรวมกันของเขตข้อมูลบางอย่างที่ไม่ซ้ำกัน แต่สิ่งเหล่านี้ไม่ได้มีความหมายพิเศษ

พวกเขายังส่งผลกระทบต่อวิธีที่คุณเขียนแบบสอบถาม (ทำให้ verbose มากขึ้นหรือน้อยลง):

  • กับ IdClass

    select t.levelStation from Time t
  • กับ EmbeddedId

    select t.timePK.levelStation from Time t

อ้างอิง

  • ข้อกำหนด JPA 1.0
    • ส่วน 2.1.4 "คีย์หลักและรหัสประจำตัว"
    • ส่วน 9.1.14 "คำอธิบายประกอบแบบฝังตัว"
    • ส่วน 9.1.15 "คำอธิบายประกอบ IdClass"

15
นอกจากนี้ยังมีโซลูชันเฉพาะของไฮเบอร์เนต: แมปคุณสมบัติหลายรายการเป็นคุณสมบัติ @Id โดยไม่ต้องประกาศคลาสภายนอกให้เป็นประเภทตัวระบุ (และใช้คำอธิบายประกอบ IdClass) ดู5.1.2.1 ตัวระบุคอมโพสิตในคู่มือไฮเบอร์เนต
Johan Boberg

โปรดลองดูคำถามนี้หน่อยได้ไหม? ฉันมีปัญหากับคีย์หลักคอมโพสิตเนื่องจากเขตข้อมูลสมาชิกidอยู่เสมอnullและไม่ได้ถูกสร้างขึ้น: /
displayname

ได้โปรดยกตัวอย่างกับผู้ทะเยอทะยานและผู้ตั้งตัวเนื่องจากฉันมีปัญหาในการดูว่าพวกเขาเข้ามาเล่นในกรณีใด โดยเฉพาะตัวอย่าง IdClass ขอบคุณ โอ้และรวมถึงชื่อคอลัมน์ด้วย
Jeremy

แม้ว่าการแก้ปัญหาเฉพาะจำศีลจะเลิก
Nikhil Sahu

จากเอกสารประกอบคำบรรยาย Hibernateเกี่ยวกับ@IdClass: "ได้รับการสืบทอดมาจากยุคมืดของ EJB 2 เพื่อความเข้ากันได้แบบย้อนหลังและเราขอแนะนำให้คุณอย่าใช้ (เพื่อความเรียบง่าย)"
มาร์โกเฟอร์รารี

49

คุณต้องใช้@EmbeddedId:

@Entity
class Time {
    @EmbeddedId
    TimeId id;

    String src;
    String dst;
    Integer distance;
    Integer price;
}

@Embeddable
class TimeId implements Serializable {
    Integer levelStation;
    Integer confPathID;
}

@ Thierry-DimitriRoy ฉันจะกำหนด timeId.levelStation และ timeId.confPathID อย่างไร คุณช่วยยกตัวอย่างได้ไหม
Duc Tran

@ Thierry-DimitriRoy คลาสหลักไม่สามารถเป็นคลาสภายในแบบคงที่ของคลาสเอนทิตีได้หรือไม่
Nikhil Sahu

ใช่อาจเป็น
Samy Omar

17

ดังที่ฉันอธิบายไว้ในบทความนี้สมมติว่าคุณมีตารางฐานข้อมูลต่อไปนี้:

ป้อนคำอธิบายรูปภาพที่นี่

ก่อนอื่นคุณต้องสร้างการ@Embeddableถือตัวระบุคอมโพสิต:

@Embeddable
public class EmployeeId implements Serializable {

    @Column(name = "company_id")
    private Long companyId;

    @Column(name = "employee_number")
    private Long employeeNumber;

    public EmployeeId() {
    }

    public EmployeeId(Long companyId, Long employeeId) {
        this.companyId = companyId;
        this.employeeNumber = employeeId;
    }

    public Long getCompanyId() {
        return companyId;
    }

    public Long getEmployeeNumber() {
        return employeeNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof EmployeeId)) return false;
        EmployeeId that = (EmployeeId) o;
        return Objects.equals(getCompanyId(), that.getCompanyId()) &&
                Objects.equals(getEmployeeNumber(), that.getEmployeeNumber());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getCompanyId(), getEmployeeNumber());
    }
}

เมื่อใช้สิ่งนี้เราสามารถแมปEmployeeเอนทิตีที่ใช้ตัวระบุคอมโพสิตโดยใส่คำอธิบายประกอบด้วย@EmbeddedId:

@Entity(name = "Employee")
@Table(name = "employee")
public class Employee {

    @EmbeddedId
    private EmployeeId id;

    private String name;

    public EmployeeId getId() {
        return id;
    }

    public void setId(EmployeeId id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Phoneนิติบุคคลซึ่งมี@ManyToOneการเชื่อมโยงไปยังEmployeeความต้องการที่จะอ้างอิงระบุคอมโพสิตจากระดับผู้ปกครองผ่านสอง@JoinColumnแมป:

@Entity(name = "Phone")
@Table(name = "phone")
public class Phone {

    @Id
    @Column(name = "`number`")
    private String number;

    @ManyToOne
    @JoinColumns({
        @JoinColumn(
            name = "company_id",
            referencedColumnName = "company_id"),
        @JoinColumn(
            name = "employee_number",
            referencedColumnName = "employee_number")
    })
    private Employee employee;

    public Employee getEmployee() {
        return employee;
    }

    public void setEmployee(Employee employee) {
        this.employee = employee;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
}

สำหรับรายละเอียดเพิ่มเติมโปรดดูบทความนี้


มีเครื่องมือที่สามารถสร้าง EmployeeId จาก db schema ได้หรือไม่?
Leon

ลองใช้เครื่องมือไฮเบอร์เนต มันมีเครื่องมือวิศวกรรมย้อนกลับสำหรับสิ่งนั้น
Vlad Mihalcea

7

คลาสคีย์หลักต้องกำหนดวิธีเท่ากับและ hashCode

  1. เมื่อใช้งานเท่ากับคุณควรใช้ instanceofเพื่ออนุญาตให้เปรียบเทียบกับคลาสย่อย ถ้า Hibernate lazy โหลดความสัมพันธ์แบบหนึ่งต่อหนึ่งหรือหลายต่อหนึ่งคุณจะมีพร็อกซีสำหรับชั้นเรียนแทนที่จะเป็นชั้นธรรมดา พร็อกซีเป็นคลาสย่อย การเปรียบเทียบชื่อคลาสจะล้มเหลว
    เทคนิคเพิ่มเติม: คุณควรปฏิบัติตามหลักการชดเชย Liskows และละเว้นความสมมาตร
  2. อันตรายต่อไปคือการใช้สิ่งที่ต้องการname.equals (that.name)แทนname.equals (that.getName ()) ครั้งแรกจะล้มเหลวถ้านั่นคือพร็อกซี

http://www.laliluna.de/jpa-hibernate-guide/ch06s06.html


6

ดูเหมือนว่าคุณกำลังทำสิ่งนี้ตั้งแต่เริ่มต้น ลองใช้เครื่องมือวิศวกรรมย้อนกลับที่มีอยู่เช่น Netbeans Entities จากฐานข้อมูลอย่างน้อยรับพื้นฐานโดยอัตโนมัติ (เช่นรหัสฝังตัว) นี่อาจกลายเป็นเรื่องปวดหัวอย่างมากหากคุณมีตารางจำนวนมาก ฉันขอแนะนำให้หลีกเลี่ยงการสร้างล้อซ้ำและใช้เครื่องมือจำนวนมากที่สุดเท่าที่จะเป็นไปได้เพื่อลดการเขียนโค้ดให้เหลือน้อยที่สุดและสำคัญที่สุดสิ่งที่คุณตั้งใจจะทำ


5

ลองยกตัวอย่างง่ายๆ สมมติว่าสองตารางชื่อtestและcustomerอธิบายไว้มีดังนี้

create table test(
  test_id int(11) not null auto_increment,
  primary key(test_id));

create table customer(
  customer_id int(11) not null auto_increment,
  name varchar(50) not null,
  primary key(customer_id));

มีอีกหนึ่งโต๊ะที่คอยติดตามข้อมูลของtestและcustomer:

create table tests_purchased(
  customer_id int(11) not null,
  test_id int(11) not null,
  created_date datetime not null,
  primary key(customer_id, test_id));

เราจะเห็นได้ว่าในตารางtests_purchasedนั้นคีย์หลักคือคีย์ผสมดังนั้นเราจะใช้<composite-id ...>...</composite-id>แท็กในhbm.xmlไฟล์การจับคู่ ดังนั้นPurchasedTest.hbm.xmlจะมีลักษณะ:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
  <class name="entities.PurchasedTest" table="tests_purchased">

    <composite-id name="purchasedTestId">
      <key-property name="testId" column="TEST_ID" />
      <key-property name="customerId" column="CUSTOMER_ID" />
    </composite-id>

    <property name="purchaseDate" type="timestamp">
      <column name="created_date" />
    </property>

  </class>
</hibernate-mapping>

แต่มันไม่จบที่นี่ ในไฮเบอร์เนตเราใช้ session.load ( entityClass, id_type_object) เพื่อค้นหาและโหลดเอนทิตีโดยใช้คีย์หลัก ในกรณีของคีย์ผสมวัตถุ ID ควรเป็นคลาส ID แยกต่างหาก (ในกรณีที่เป็นPurchasedTestIdคลาส) ซึ่งเพิ่งประกาศแอตทริบิวต์คีย์หลักเช่นด้านล่าง :

import java.io.Serializable;

public class PurchasedTestId implements Serializable {
  private Long testId;
  private Long customerId;

  // an easy initializing constructor
  public PurchasedTestId(Long testId, Long customerId) {
    this.testId = testId;
    this.customerId = customerId;
  }

  public Long getTestId() {
    return testId;
  }

  public void setTestId(Long testId) {
    this.testId = testId;
  }

  public Long getCustomerId() {
    return customerId;
  }

  public void setCustomerId(Long customerId) {
    this.customerId = customerId;
  }

  @Override
  public boolean equals(Object arg0) {
    if(arg0 == null) return false;
    if(!(arg0 instanceof PurchasedTestId)) return false;
    PurchasedTestId arg1 = (PurchasedTestId) arg0;
    return (this.testId.longValue() == arg1.getTestId().longValue()) &&
           (this.customerId.longValue() == arg1.getCustomerId().longValue());
  }

  @Override
  public int hashCode() {
    int hsCode;
    hsCode = testId.hashCode();
    hsCode = 19 * hsCode+ customerId.hashCode();
    return hsCode;
  }
}

จุดสำคัญคือเรายังใช้ทั้งสองฟังก์ชั่นhashCode()และequals()ในขณะที่ Hibernate อาศัย


2

ตัวเลือกอื่นคือการแมปเป็นแผนที่ขององค์ประกอบประกอบในตาราง ConfPath

การทำแผนที่นี้จะได้ประโยชน์จากดัชนีใน (ConfPathID, levelStation)

public class ConfPath {
    private Map<Long,Time> timeForLevelStation = new HashMap<Long,Time>();

    public Time getTime(long levelStation) {
        return timeForLevelStation.get(levelStation);
    }

    public void putTime(long levelStation, Time newValue) {
        timeForLevelStation.put(levelStation, newValue);
    }
}

public class Time {
    String src;
    String dst;
    long distance;
    long price;

    public long getDistance() {
        return distance;
    }

    public void setDistance(long distance) {
        this.distance = distance;
    }

    public String getDst() {
        return dst;
    }

    public void setDst(String dst) {
        this.dst = dst;
    }

    public long getPrice() {
        return price;
    }

    public void setPrice(long price) {
        this.price = price;
    }

    public String getSrc() {
        return src;
    }

    public void setSrc(String src) {
        this.src = src;
    }
}

การทำแผนที่:

<class name="ConfPath" table="ConfPath">
    <id column="ID" name="id">
        <generator class="native"/>
    </id>
    <map cascade="all-delete-orphan" name="values" table="example"
            lazy="extra">
        <key column="ConfPathID"/>
        <map-key type="long" column="levelStation"/>
        <composite-element class="Time">
            <property name="src" column="src" type="string" length="100"/>
            <property name="dst" column="dst" type="string" length="100"/>
            <property name="distance" column="distance"/>
            <property name="price" column="price"/>
        </composite-element>
    </map>
</class>

1

ใช้ hbm.xml

    <composite-id>

        <!--<key-many-to-one name="productId" class="databaselayer.users.UserDB" column="user_name"/>-->
        <key-property name="productId" column="PRODUCT_Product_ID" type="int"/>
        <key-property name="categoryId" column="categories_id" type="int" />
    </composite-id>  

การใช้คำอธิบายประกอบ

คลาสคีย์ผสม

public  class PK implements Serializable{
    private int PRODUCT_Product_ID ;    
    private int categories_id ;

    public PK(int productId, int categoryId) {
        this.PRODUCT_Product_ID = productId;
        this.categories_id = categoryId;
    }

    public int getPRODUCT_Product_ID() {
        return PRODUCT_Product_ID;
    }

    public void setPRODUCT_Product_ID(int PRODUCT_Product_ID) {
        this.PRODUCT_Product_ID = PRODUCT_Product_ID;
    }

    public int getCategories_id() {
        return categories_id;
    }

    public void setCategories_id(int categories_id) {
        this.categories_id = categories_id;
    }

    private PK() { }

    @Override
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }

        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }

        PK pk = (PK) o;
        return Objects.equals(PRODUCT_Product_ID, pk.PRODUCT_Product_ID ) &&
                Objects.equals(categories_id, pk.categories_id );
    }

    @Override
    public int hashCode() {
        return Objects.hash(PRODUCT_Product_ID, categories_id );
    }
}

คลาสเอนทิตี

@Entity(name = "product_category")
@IdClass( PK.class )
public  class ProductCategory implements Serializable {
    @Id    
    private int PRODUCT_Product_ID ;   

    @Id 
    private int categories_id ;

    public ProductCategory(int productId, int categoryId) {
        this.PRODUCT_Product_ID = productId ;
        this.categories_id = categoryId;
    }

    public ProductCategory() { }

    public int getPRODUCT_Product_ID() {
        return PRODUCT_Product_ID;
    }

    public void setPRODUCT_Product_ID(int PRODUCT_Product_ID) {
        this.PRODUCT_Product_ID = PRODUCT_Product_ID;
    }

    public int getCategories_id() {
        return categories_id;
    }

    public void setCategories_id(int categories_id) {
        this.categories_id = categories_id;
    }

    public void setId(PK id) {
        this.PRODUCT_Product_ID = id.getPRODUCT_Product_ID();
        this.categories_id = id.getCategories_id();
    }

    public PK getId() {
        return new PK(
            PRODUCT_Product_ID,
            categories_id
        );
    }    
}

1
มันไม่สมเหตุสมผลเขาต้องการคีย์หลัก
Mazen Embaby

ในชื่อเขากล่าวว่าคีย์ผสมซึ่งไม่จำเป็นต้องเป็นหลัก
Enerccio

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