【问题标题】:How to map a composite key with JPA and Hibernate?如何使用 JPA 和 Hibernate 映射复合键?
【发布时间】:2023-04-03 17:17:02
【问题描述】:

在这段代码中,如何为复合键生成一个Java类(如何在hibernate中复合键):

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;

【问题讨论】:

标签: java hibernate jpa orm composite-key


【解决方案1】:

要映射复合键,您可以使用EmbeddedId IdClass 注释。我知道这个问题并不严格与 JPA 有关,但规范定义的规则也适用。所以他们在这里:

2.1.4 主键和实体标识

...

复合主键必须 对应于任何一个 持久性字段或属性或 一组这样的字段或属性 如下面所描述的。主键类 必须定义为代表一个 复合主键。合成的 主键通常出现在 从遗留数据库映射时 数据库密钥由几个 列。 EmbeddedIdIdClass 注解用于 表示复合主键。请参阅 第 9.1.14 和 9.1.15 节。

...

以下规则适用于 复合主键:

  • 主键类必须是公共的,并且必须有一个公共的无参数 构造函数。
  • 如果使用基于属性的访问,则主键的属性 类必须是公共的或受保护的。
  • 主键类必须是serializable
  • 主键类 必须定义equalshashCode 方法。 值的语义 这些方法的相等性必须是 与数据库相等性一致 对于数据库类型 键已映射。
  • 复合主键必须表示和映射为 可嵌入类(参见第 9.1.14 节, “EmbeddedId Annotation”)或必须是 表示并映射到多个 实体的字段或属性 类(参见第 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 有助于指定某些字段组合是唯一的,但它们没有特殊含义

它们还会影响您编写查询的方式(使它们或多或少冗长):

  • IdClass

    select t.levelStation from Time t
    
  • EmbeddedId

    select t.timePK.levelStation from Time t
    

参考文献

  • JPA 1.0 规范
    • 第 2.1.4 节“主键和实体身份”
    • 第 9.1.14 节“EmbeddedId 注释”
    • 第 9.1.15 节“IdClass 注释”

【讨论】:

  • 还有一个特定于 Hibernate 的解决方案:将多个属性映射为 @Id 属性而不将外部类声明为标识符类型(并使用 IdClass 注释)。请参阅 Hibernate 手册中的 5.1.2.1. Composite identifier
  • 请看一下this question好吗?我在使用复合主键时遇到问题,因为成员字段 id 始终为 null 并且不会生成:/
  • 能否举一个 getter 和 setter 的例子,因为在这两种情况下我都很难看到它们在哪里发挥作用。尤其是 IdClass 示例。谢谢。哦,包括列名,谢谢。
  • 虽然休眠特定的解决方案已被弃用。
  • 来自Hibernate Annotations docs,关于@IdClass:“它继承自EJB 2 的黑暗时代以实现向后兼容性,我们建议您不要使用它(为简单起见)。”
【解决方案2】:

你需要使用@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。可以举个例子吗?
  • @Thierry-DimitriRoy 主类不能是实体类的静态内部类吗?
  • 是的,可能是
【解决方案3】:

假设您有以下数据库表:

首先,您需要创建包含复合标识符的@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;
    }
}

Employee@ManyToOne关联的Phone实体需要通过两个@JoinColumnmappings从父类引用复合标识符:

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

【讨论】:

  • 有没有工具可以从db schema 生成EmployeeId?
  • 试试休眠工具。它有一个逆向工程工具。
【解决方案4】:

主键类必须定义equals和hashCode方法

  1. 在实现 equals 时,您应该使用 instanceof 以允许与子类进行比较。如果 Hibernate 延迟加载一对一或多对一关系,您将拥有该类的代理而不是普通类。代理是一个子类。比较类名会失败。
    从技术上讲:您应该遵循 Liskows 替代原则并忽略对称性。
  2. 下一个陷阱是使用 name.equals(that.name) 代替 name.equals(that.getName())。如果那是代理,第一个将失败。

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

【讨论】:

    【解决方案5】:

    看起来您是从头开始执行此操作的。尝试使用可用的逆向工程工具,例如来自数据库的 Netbeans 实体,至少使基础自动化(例如嵌入式 ID)。如果您有很多桌子,这可能会变得非常头疼。我建议避免重新发明轮子,并使用尽可能多的可用工具来将编码减少到最小和最重要的部分,即您打算做什么。

    【讨论】:

      【解决方案6】:

      让我们举一个简单的例子。假设有两个名为 testcustomer 的表被描述为:

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

      还有一张表格记录了tests 和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表中的主键是一个复合键,所以我们将在hbm.xml映射文件中使用<composite-id ...>...</composite-id>标签。所以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>
      

      但这并没有结束。在 Hibernate 中,我们使用 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依赖它们。

      【讨论】:

        【解决方案7】:

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

        【讨论】:

        • 没有意义,他需要主键
        • 在标题中他说复合键,不一定是主键
        • 请检查他写的sql 主键(levelStation, confPathID)
        【解决方案8】:

        另一种选择是将映射作为 ConfPath 表中复合元素的 Map。

        不过,此映射将受益于 (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>
        

        【讨论】:

          猜你喜欢
          • 2011-03-20
          • 2017-01-30
          • 2016-09-18
          • 2012-04-06
          • 2017-02-21
          • 1970-01-01
          • 2010-09-09
          • 1970-01-01
          • 2016-06-05
          相关资源
          最近更新 更多