|
近期遇到一个ORM映射需求,即在“多-多”关系中需要将对应到两端实体的外键作为关系的联合主键,查了N多资料,试了N种方法,终于找到了自认为比较合理的ORM映射方式,在这里贴出来,与大家分享一下。
1、问题描述
这是一个古老的订单问题,涉及到的对象有3个,分别为:订单,订单项,产品。订单与产品间是“多-一”关系,它们间的关系通过订单项来反映:一个订单包含多个订单项(进而包括多种产品),一个订单项对应到一种产品。实际的数据库表结构如下所示(为了简化起见省略了其它的关系字段)。
- 订单表Orders。包括字段:Order ID,Order Amount,Order Date,Required Date,Ship Date,Courier Website,Ship Via,Shipped,PO#,Payment Received。
- 产品表Product。包括字段:Product ID,Product Name,Color,Size,M/F,Price (SRP),Product Class。
- 订单项表Orders Detail。Order ID,Product ID,Unit Price,Quantity。其中Order ID为到订单表(Orders)的外键,Product ID为到产品表(Product)的外键,并且它们放在一起作为订单项表的联合主键。
现在的需求是实现这几个实体及其相互关系的ORM映射,并且不能改变数据库的表结构。
2、解决方案
由于在订单项表中的2个外键需要同时承担联合主键的职责,因此感觉不是很好处理。我采用了下面的解决办法,即:为订单项设立一个联合主键类,同时在主键类中实现外键映射。
下面是订单类Order的代码,其中的映射使用JPA的Annotation来描述的。
@Entity
@Table(name
=
"
Orders
"
)

public
class
Order
...
{

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "[Order ID]")
private Integer orderID;

@Column(name = "[Order Amount]")
private Double orderAmount;

@Column(name = "[Order Date]")
@Temporal(value = TemporalType.DATE)
private Date orderDate;

@Column(name = "[Required Date]")
private Date requiredDate;

@Column(name = "[Ship Date]")
private Date shipDate;

@Column(name = "[Courier Website]")
private String courierWebsite;

@Column(name = "[Ship Via]")
private String shipVia;

@Column(name = "Shipped")
private Boolean Shipped;

@Column(name = "[PO#]")
private String pO;

@Column(name = "[Payment Received]")
private Boolean paymentReceived;

@OneToMany(mappedBy = "pk.order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<OrderDetail> orderDetails;

 public Integer getOrderID() ...{
return orderID;
}

 public void setOrderID(Integer orderID) ...{
this.orderID = orderID;
}

 public Double getOrderAmount() ...{
return orderAmount;
}

 public void setOrderAmount(Double orderAmount) ...{
this.orderAmount = orderAmount;
}

 public Date getOrderDate() ...{
return orderDate;
}

 public void setOrderDate(Date orderDate) ...{
this.orderDate = orderDate;
}

 public Date getRequiredDate() ...{
return requiredDate;
}

 public void setRequiredDate(Date requiredDate) ...{
this.requiredDate = requiredDate;
}

 public Date getShipDate() ...{
return shipDate;
}

 public void setShipDate(Date shipDate) ...{
this.shipDate = shipDate;
}

 public String getCourierWebsite() ...{
return courierWebsite;
}

 public void setCourierWebsite(String courierWebsite) ...{
this.courierWebsite = courierWebsite;
}

 public String getShipVia() ...{
return shipVia;
}

 public void setShipVia(String shipVia) ...{
this.shipVia = shipVia;
}

 public Boolean getShipped() ...{
return Shipped;
}

 public void setShipped(Boolean shipped) ...{
Shipped = shipped;
}

 public String getPO() ...{
return pO;
}

 public void setPO(String po) ...{
pO = po;
}

 public Boolean getPaymentReceived() ...{
return paymentReceived;
}

 public void setPaymentReceived(Boolean paymentReceived) ...{
this.paymentReceived = paymentReceived;
}

 public Set<OrderDetail> getOrderDetails() ...{
Session session = HibernateUtil.currentSession();
session.beginTransaction();
session.lock(this, LockMode.NONE);
Set<OrderDetail> orderDetails = this.orderDetails;
return orderDetails;
}

 public void setOrderDetails(Set<OrderDetail> orderDetails) ...{
this.orderDetails = orderDetails;
}

@Override
 public int hashCode() ...{
final int prime = 31;
int result = 1;
result = prime * result + ((orderID == null) ? 0 : orderID.hashCode());
return result;
}

@Override
 public boolean equals(Object obj) ...{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Order other = (Order) obj;
 if (orderID == null) ...{
if (other.orderID != null)
return false;
} else if (!orderID.equals(other.orderID))
return false;
return true;
}

}
下面是产品类Product的代码。
@Entity
@Table(name
=
"
Product
"
)

public
class
Product
...
{

@Id
@Column(name = "[Product ID]")
private Integer productID;

@Column(name = "[Product Name]")
private String productName;

@Column(name = "Color")
private String color;

@Column(name = "Size")
private String size;

@Column(name = "[M/F]")
private String m_F;

@Column(name = "[Price (SRP)]")
private double price_SRP;

@Column(name = "[Product Class]")
private String productClass;

 public Integer getProductID() ...{
return productID;
}

 public void setProductID(Integer productID) ...{
this.productID = productID;
}

 public String getProductName() ...{
return productName;
}

 public void setProductName(String productName) ...{
this.productName = productName;
}

 public String getColor() ...{
return color;
}

 public void setColor(String color) ...{
this.color = color;
}

 public String getSize() ...{
return size;
}

 public void setSize(String size) ...{
this.size = size;
}

 public String getM_F() ...{
return m_F;
}

 public void setM_F(String m_f) ...{
m_F = m_f;
}

 public double getPrice_SRP() ...{
return price_SRP;
}

 public void setPrice_SRP(double price_SRP) ...{
this.price_SRP = price_SRP;
}

 public String getProductClass() ...{
return productClass;
}

 public void setProductClass(String productClass) ...{
this.productClass = productClass;
}

@Override
 public int hashCode() ...{
final int prime = 31;
int result = 1;
result = prime * result
+ ((productID == null) ? 0 : productID.hashCode());
return result;
}

@Override
 public boolean equals(Object obj) ...{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Product other = (Product) obj;
 if (productID == null) ...{
if (other.productID != null)
return false;
} else if (!productID.equals(other.productID))
return false;
return true;
}

}
注意到产品类中并没有订单项对象,从实际的逻辑上来考虑产品并不知道订单项的存在,因此这样设计成单向的关系是合理的。
下面是订单项主键类OrderDetailPK的源代码。
@Embeddable

public
class
OrderDetailPK
implements
Serializable
...
{

@ManyToOne(cascade = CascadeType.ALL,fetch=FetchType.LAZY)
@JoinColumn(name = "[Order ID]")
Order order;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "[Product ID]")
private Product product;

 public OrderDetailPK() ...{

}

 public OrderDetailPK(Order order, Product product) ...{
this.order = order;
this.product = product;
}

 public Order getOrder() ...{
return order;
}

 public void setOrder(Order order) ...{
this.order = order;
}

 public Product getProduct() ...{
return product;
}

 public void setProduct(Product product) ...{
this.product = product;
}

@Override
 public int hashCode() ...{
final int prime = 31;
int result = 1;
result = prime * result + ((order == null) ? 0 : order.hashCode());
result = prime * result + ((product == null) ? 0 : product.hashCode());
return result;
}

@Override
 public boolean equals(Object obj) ...{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final OrderDetailPK other = (OrderDetailPK) obj;
 if (order == null) ...{
if (other.order != null)
return false;
} else if (!order.equals(other.order))
return false;
 if (product == null) ...{
if (other.product != null)
return false;
} else if (!product.equals(other.product))
return false;
return true;
}

}private
作为联合主键类,该类必须实现equals方法和hashcode方法。注意到该类的关键部分是:
@ManyToOne(cascade = CascadeType.ALL,fetch=FetchType.LAZY)
@JoinColumn(name = "[Order ID]")
private Order order;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "[Product ID]")
private Product product;
在该类中定义了2个属性,分别用来表示到Order和Product的映射。这种主键类定义方式是符合逻辑的:订单项由产品和订单共同唯一确定,且从订单项可以导航到相应的订单和产品信息。
以上面的主键类作为主键,订单项类OrderDetail的代码如下所示。
@Entity
@Table(name
=
"
[Orders Detail]
"
)

public
class
OrderDetail
...
{

@Column(name = "[Unit Price]")
private Double unitPrice;

@Column(name = "Quantity")
private Integer quantity;

@EmbeddedId
private OrderDetailPK pk=new OrderDetailPK();

 public Double getUnitPrice() ...{
return unitPrice;
}

 public void setUnitPrice(Double unitPrice) ...{
this.unitPrice = unitPrice;
}

 public Integer getQuantity() ...{
return quantity;
}

 public void setQuantity(Integer quantity) ...{
this.quantity = quantity;
}

@Transient
 public Order getOrder() ...{
Session session = HibernateUtil.currentSession();
session.beginTransaction();
session.lock(this, LockMode.NONE);
Order order=this.pk.getOrder();
return order;
}

 public void setOrder(Order order) ...{
this.pk.setOrder(order);
}

@Transient
 public Product getProduct() ...{
Session session = HibernateUtil.currentSession();
session.beginTransaction();
session.lock(this, LockMode.NONE);
Product product=this.pk.getProduct();
return product;
}

 public void setProduct(Product product) ...{
this.pk.setProduct(product);
}
}
在该类中包含了刚才定义的订单项主键类,同时暴露出了瞬态属性order和product(通过get方法和set方法),这两个属性从主键类中获取相应的实体对象,代码是:
@Transient
 public Order getOrder() ...{
Session session = HibernateUtil.currentSession();
session.beginTransaction();
session.lock(this, LockMode.NONE);
Order order=this.pk.getOrder();
return order;
}

 public void setOrder(Order order) ...{
this.pk.setOrder(order);
}

@Transient
 public Product getProduct() ...{
Session session = HibernateUtil.currentSession();
session.beginTransaction();
session.lock(this, LockMode.NONE);
Product product=this.pk.getProduct();
return product;
}

 public void setProduct(Product product) ...{
this.pk.setProduct(product);
}
3、总结
在数据库中如果存在多-多关系,该关系需要单独映射成实体,并且需要在该关系中使用关系的两端外键作为关系的联合主键,则可以采用本文所述的这种映射方式:为联合外键构建相应的主键类,并在主键类中定义外键映射,最后把主键类作为嵌入式主键嵌入到实体中。 |