JPA 诞生的原因
面向对象编程的问题之一,就是如何在数据库与对象之间产生对应关系。例如你有一个Car类,但你数据库整的表名却叫TB_CAR。这还没完,又例如,Car类中有个属性叫name,但表中的字段却叫STR_NAME_CAR。
用JDBC的话,需要很多手工对应:
import java.sql.*;
import java.util.LinkedList;
import java.util.List;
public class MainWithJDBC {
public static void main(String[] args) throws Exception {
Class.forName("org.hsqldb.jdbcDriver");
Connection connection = // get a valid connection
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery("SELECT \"Id\", \"Name\" FROM \"Car\"");
List<Car> cars = new LinkedList<Car>();
while (rs.next()) {
Car car = new Car();
car.setId(rs.getInt("Id"));
car.setName(rs.getString("Name"));
cars.add(car);
}
for (Car car : cars) {
System.out.println("Car id: " + car.getId() + " Car Name: " + car.getName());
}
connection.close();
}
}
如你所见,其中有很多样板代码。想想你有一个拥有30个属性的类,而且里面组合了另一个也是有30个属性的类。就像Car类有一个对应的司机列表,司机所属的Person类有30个属性……
JDBC的另一个缺点就是移植性。你的 sql 语法需要因应不同的数据库而改变。例如 ORACLE 用 ROWNUM 来控制返回的行数,而SQL SERVER 用TOP 。
使用原生的查询语句所造成的移植性问题,解决方法之一如下:将查询语句独立出来放在一个文件中,如果切换不同的数据库,就需要多份语法与之对应的文件。
还有一些其他问题,如:修改表关系,级联删除、更新……
什么是 JPA 以及 JPA 实现?
JPA 是以上问题的一个解决方案。
通过JPA ,我们无需关注不同数据库的语义细节。
JPA 就是 “JPA 实现”的一套规范,它包括文本、规则、接口。JPA 实现有很多种,无论免费的还是收费的:Hibernate,OpenJPA,EclipseLink,Batoo 等等。
大多数 JPA 实现都允许扩展代码和注解,但必须遵照 JPA 的规范。
JPA 的主要功能就是将数据库与类对应,并以此处理移植性问题。后面章节将演示如何将表字段映射到类属性,而无需顾及他们名字的差异。
JPA 有一种数据库查询语言,叫 JPQL 。它的优点就是能处理各种不同的数据库。
SELECT id, name, color, age, doors FROM Car
以上查询语句能转换成以下 JPQL:
SELECT c FROM Car c
注意查询结果是 Car 类的对象 c ,而不是什么字段或属性。JPA 会自动封装这个对象。
JPA 负责将 JPQL 转换成具体的原生查询语句,开发者不用再担心数据库的语法差异。
persistence.xml 及其中的配置有什么用?
它包含所有 JPA 环境的配置,必须放在 META-INF 中。以下是 eclipse 的例子,netbean 另外再看。
如果你遇到“Could not find any META-INF/persistence.xml file in the classpath”这种提示,以下是排查贴士:
- 检查它是否在 war 文件的 /META-INF 中。如果 war 由 IDE 生成,检查配置文件是否已放在 IDE 规定的目录中。如果是手工生成,检查生成脚本中是否有遗漏(ant 、 maven)。
- 检查名字是否 persistence.xml (全小写)。
以下是 persistence.xml 的例子:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="MyPU" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>page20.Person</class>
<class>page20.Cellular</class>
<class>page20.Call</class>
<class>page20.Dog</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbcDriver" />
<property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:mem:myDataBase" />
<property name="javax.persistence.jdbc.user" value="sa" />
<property name="javax.persistence.jdbc.password" value="" />
<property name="eclipselink.ddl-generation" value="create-tables" />
<property name="eclipselink.logging.level" value="FINEST" />
</properties>
</persistence-unit>
<persistence-unit name="PostgresPU" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>page26.Car</class>
<class>page26.Dog</class>
<class>page26.Person</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost/JpaRelationships"
/>
<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
<property name="javax.persistence.jdbc.user" value="postgres" />
<property name="javax.persistence.jdbc.password" value="postgres" />
<!-- <property name="eclipselink.ddl-generation" value="drop-and-create-tables" /> -->
<property name="eclipselink.ddl-generation" value="create-tables" />
<!-- <property name="eclipselink.logging.level" value="FINEST" /> -->
</properties>
</persistence-unit>
</persistence>
解释:
- persistence-unit name=”MyPU” 配置持久化单元的名字。持久化单元就是一个 JPA 的大环境,它包含了应用中与数据库相关的类、关系、键等信息。一个persistence.xml 中可以有多个持久化单元。
- transaction-type=”RESOURCE_LOCAL” 定义事务类型,可选RESOURCE_LOCAL 和 JTA 。桌面应用,要选择RESOURCE_LOCAL 。web 应用的话,两者皆可,具体看你应用如何设计。
- <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> 定义 JPA 实现的提供方,如果你用Hibernate,就填 org.hibernate.ejb.HibernatePersistence ,用OpenJPA 就填 org.apache.openjpa.persistence.PersistenceProviderImpl 。
- <class></class> 用于定义 java 类,一般没用。Hibernate 是不用的,但 EclipseLink 和 OpenJPA 需要。
- <exclude-unlisted-classes>true</exclude-unlisted-classes> 用于定义是否不把未列明的类当做实体(实体在后面介绍)。对于不同持久化单元拥有不同实体的应用来说,此标签很有用。
- <!– –> 注解。
- <properties> 用于填写driver、password、user等。对于RESOUCE_LOCAL ,就写在该 xml 中。而JTA ,它的数据源由容器管理,数据源用<jta-data-source></jta-data-source><non-jta-data-source></non-jta-data-source> 定义。properties标签还可以控制表的 DDL ,以及 LOG的级别。
定义实体
实体用于数据库与类的相互映射,其结构要与表相同。
作为实体的 java 类需要满足以下规则:
- 带有 @Entity 注解
- 带有 public 的、无参的构造函数
- 其中一个属性带有 @Id 注解
以下是一个能作为实体的 java 类:
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Car {
public Car() {
}
public Car(int id) {
this.id = id;
}
// Just to show that there is no need to have get/set when we talk about JPA Id
@Id
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
解释:
- 类名注解 @Entity
- 每个实体都要有 id,所以需要有一个属性带有 @Id。通常属性是顺序的整型数,但也可以是字符串类型
- 注意 id 没有对应的 get/set 方法,因为根据 JPA 规范,id 是不可变的。
- public 的无参构造函数是必须的,其他构造函数可选。
代码示例中仅用到了两个注解,这样的话,JPA 就会根据类名 Car 去数据库找表,根据属性 id 和 name 去找表字段 ID 和 NAME。
按《pro jpa 2》的说法,JPA 的注解分为逻辑注解和物理注解。逻辑注解定义用途,物理注解定义映射。
例如:
import java.util.List;
import javax.persistence.*;
@Entity
@Table(name = "TB_PERSON_02837")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Basic(fetch = FetchType.LAZY)
@Column(name = "PERSON_NAME", length = 100, unique = true, nullable = false)
private String name;
@OneToMany
private List<Car> cars;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
以上,@Entity,@Id 和 @OneToMany 都是逻辑注解,他们并没定义类与数据库的关系,只是说明该类是作为实体而存在。
而另外,@Table,@Column 和 @Basic,则映射了表名、字段名等与数据库相关的信息,属于物理注解。
id 的生成:定义,自增式,序列式
JPA 能用以下三种方式自动为实体产生 id:
- 自增式
- 序列式
- 表生成
不同数据库支持不同的 id 生成机制,oracle 和 postgres 用序列式,sql server 和 mysql 用自增式。
自增式
自增式最简单,只需这样写注解:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这种 id 的生成方式就由数据库控制,JPA 对其无任何影响。因此,为了得到 id ,必须先做持久化并提交,然后 JPA 再做一次查询,来获取生成的 id 。这样会有些影响性能,但问题其实不大。
它不能在内存中分配 id,稍后解释。
序列式
这样配置:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
@Entity
@SequenceGenerator(name = Car.CAR_SEQUENCE_NAME, sequenceName = Car.CAR_SEQUENCE_NAME, initialValue = 10, allocationSize = 53)
public class Car {
public static final String CAR_SEQUENCE_NAME = "CAR_SEQUENCE_ID";
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = CAR_SEQUENCE_NAME)
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
解释:
- @SequenceGenerator 的 sequenceName 属性定义了数据库中的序列名。同一个序列能应用于不同实体,但这种做法并不推荐,因为会造成表里的 id 不连续。
- name = Car.CAR_SEQUENCE_NAME 定义了应用中的序列名。所有该类的实体都该共用同一个序列,所以用 final statis 修饰。
- sequenceName = Car.CAR_SEQUENCE_NAME 定义了数据库中的序列名。
- initialValue = 10 定义了该序列的首次取值。容易出错的地方时,如果此处有定义,则每次应用启动,都会按此定义而非数据库序列的实际进度来取值。这会造成重复 id。
- allocationSize 定义了 JPA 会缓存的后续 id 的数量。例子中缓存了 53个。这样可以减轻与数据库交互的压力,不像自增式那样。
- @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = CAR_SEQUENCE_NAME) 定义了 id 的产生策略为序列式,以及所用到的序列
id 的生成:表生成,自动
表生成
其机制如下:
用一个表来存放表名及 id
能避免依赖自增式或序列式,以提供移植性。
import javax.persistence.*;
@Entity
public class Person {
@Id
@TableGenerator(name = "TABLE_GENERATOR", table = "ID_TABLE", pkColumnName = "ID_TABLE_NAME", pkColumnValue = "PERSON_ID", valueColumnName = "ID_TABLE_VALUE")
@GeneratedValue(strategy = GenerationType.TABLE, generator = "TABLE_GENERATOR")
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
以上代码会使用下图中的表(该表也可以通过 persistence.xml 自动创建)
代码解释:
- pkColumnName,存放 id 名的列
- pkColumnValue,id 名
- valueColumnName,id 值
- initialValue he allocationSize 也可以用
- 不同的类的 id,用同一个表生成,只要 id 不同,是不会产生跳跃的 id 的。
表生成的最佳实践是使用 orm.xml ,而非注解。本书不详解。
自动生成
就是让 JPA 替你选择。
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // or just @GeneratedValue
private int id;
JPA 会从以上三种选出一种来实现。
简单组合键
简单键就是 id 只由一个属性(字段)构成。组合键由多属性构成。简单组合键只使用 java 自带的类型作为属性类型。
可以使用@IdClass 或者 @EmbeddedId 来定义简单组合键
@IdClass
且看以下代码:
import javax.persistence.*;
@Entity
@IdClass(CarId.class)
public class Car {
@Id
private int serial;
@Id
private String brand;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSerial() {
return serial;
}
public void setSerial(int serial) {
this.serial = serial;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
解释:
- @IdClass(CarId.class) 定义了Car 的 id 能从 CarId 中找到。
- 所有 @Id 都要在 IdClass 中
- 简单组合键也可以使用 @GeneratedValue
再看 CarId 类:
import java.io.Serializable;
public class CarId implements Serializable {
private static final long serialVersionUID = 343L;
private int serial;
private String brand;
// must have a default construcot
public CarId() {
}
public CarId(int serial, String brand) {
this.serial = serial;
this.brand = brand;
}
public int getSerial() {
return serial;
}
public String getBrand() {
return brand;
}
// Must have a hashCode method
@Override
public int hashCode() {
return serial + brand.hashCode();
}
// Must have an equals method
@Override
public boolean equals(Object obj) {
if (obj instanceof CarId) {
CarId carId = (CarId) obj;
return carId.serial == this.serial && carId.brand.equals(this.brand);
}
return false;
}
}
这种类需要符合以下规则:
- public的、无参的构造函数
- 实现 Serializable 接口
- 重写hashCode 和 equals 方法
用简单组合键去数据库查找这个实体:
EntityManager em = // get valid entity manager
CarId carId = new CarId(33, "Ford");
Car persistedCar = em.find(Car.class, carId);
System.out.println(persistedCar.getName() + " - " + persistedCar.getSerial());
@Embeddable
另一种定义简单组合键的方法如下:
import javax.persistence.*;
@Entity
public class Car {
@EmbeddedId
private CarId carId;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public CarId getCarId() {
return carId;
}
public void setCarId(CarId carId) {
this.carId = carId;
}
}
解释:
- id 类组合到实体中
- @EmbeddedId 用于指明 id
- 不需要 @Id 了
id 类变成这样:
import java.io.Serializable;
import javax.persistence.Embeddable;
@Embeddable
public class CarId implements Serializable {
private static final long serialVersionUID = 343L;
private int serial;
private String brand;
// must have a default construcot
public CarId() {
}
public CarId(int serial, String brand) {
this.serial = serial;
this.brand = brand;
}
public int getSerial() {
return serial;
}
public String getBrand() {
return brand;
}
// Must have a hashCode method
@Override
public int hashCode() {
return serial + brand.hashCode();
}
// Must have an equals method
@Override
public boolean equals(Object obj) {
if (obj instanceof CarId) {
CarId carId = (CarId) obj;
return carId.serial == this.serial && carId.brand.equals(this.brand);
}
return false;
}
}
解释:
- @Embeddable 表明该类能作为 id
- 该类的属性会被当做组合 id
同样需要符合以下规则:
- public的、无参的构造函数
- 实现 Serializable 接口
- 重写hashCode 和 equals 方法
复杂组合键
复杂组合键由其他实体组成,不只是普通的 java 自带类型。
想想狗屋这个实体以狗来作为 id :
import javax.persistence.*;
@Entity
public class Dog {
@Id
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
import javax.persistence.*;
@Entity
public class DogHouse {
@Id
@OneToOne
@JoinColumn(name = "DOG_ID")
private Dog dog;
private String brand;
public Dog getDog() {
return dog;
}
public void setDog(Dog dog) {
this.dog = dog;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
解释:
- DogHouse 的 @Id 表明狗屋使用狗的 id 作为自己的 id。
- @Id 与 @OneToOne 同时使用,指定两个实体间的关系(稍后详谈)
但如果想直接获取狗屋的 id 而不通过狗(dogHouse.getDog().getId())呢? JPA 能避开Demeter 法则:
import javax.persistence.*;
@Entity
public class DogHouseB {
@Id
private int dogId;
@MapsId
@OneToOne
@JoinColumn(name = "DOG_ID")
private Dog dog;
private String brand;
public Dog getDog() {
return dog;
}
public void setDog(Dog dog) {
this.dog = dog;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public int getDogId() {
return dogId;
}
public void setDogId(int dogId) {
this.dogId = dogId;
}
}
解释:
- 有了一个明确的 @Id
- @MapsId 使得 Dog.id 与 DogHouse.dogId 对应,并在运行时,dogId 被赋值。
- 不必要指定 dogId 的列名。
再看看如何使一个实体与多个实体对应:
import javax.persistence.*;
@Entity
@IdClass(DogHouseId.class)
public class DogHouse {
@Id
@OneToOne
@JoinColumn(name = "DOG_ID")
private Dog dog;
@Id
@OneToOne
@JoinColumn(name = "PERSON_ID")
private Person person;
private String brand;
// get and set
}
解释:
- 实体Dog 和 Person 都标记了 @Id。
- 注解 @IdClass 将两个 @Id 合成简单组合键
import java.io.Serializable;
public class DogHouseId implements Serializable {
private static final long serialVersionUID = 1L;
private int person;
private int dog;
public int getPerson() {
return person;
}
public void setPerson(int person) {
this.person = person;
}
public int getDog() {
return dog;
}
public void setDog(int dog) {
this.dog = dog;
}
@Override
public int hashCode() {
return person + dog;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DogHouseId) {
DogHouseId dogHouseId = (DogHouseId) obj;
return dogHouseId.dog == dog && dogHouseId.person == person;
}
return false;
}
}
解释:
- 这个 ID 类的属性数量与狗屋实体的 id 数量相同,名字也相同,这是必要的,为了使 JPA 能将他们对应起来
同样,它也要遵循 ID 类的规范。
如何获取实体管理器
两种方法:注入、工厂方法。
注入:
@PersistenceContext(unitName = "PERSISTENCE_UNIT_MAPPED_IN_THE_PERSISTENCE_XML")
private EntityManager entityManager;
“注入”只能在带有EJB容器的应用服务器,如 JBOSS、GLASSFISH 中使用。
如果需要应用来操控 connection , 则使用工厂方法:
EntityManagerFactory emf = Persistence.createEntityManagerFactory("PERSISTENCE_UNIT_MAPPED_IN_THE_PERSISTENCE_XML");
EntityManager entityManager = emf.createEntityManager();
entityManager.getTransaction().begin();
// do something
entityManager.getTransaction().commit();
entityManager.close();
注意要先获取实体管理器工厂—— EntityManagerFactory ,并且与 persistence.xml 中的持久化单元对应。
在一个实体里映射两个甚至多个表
只需这样做:
import javax.persistence.*;
@Entity
@Table(name = "DOG")
@SecondaryTables({
@SecondaryTable(name = "DOG_SECONDARY_A", pkJoinColumns = {@PrimaryKeyJoinColumn(name = "DOG_ID")}),
@SecondaryTable(name = "DOG_SECONDARY_B", pkJoinColumns = {@PrimaryKeyJoinColumn(name = "DOG_ID")})
})
public class Dog {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
private int age;
private double weight;
// get and set
}
解释:
如果只有两个表,则 @SecundaryTable 就足够了。
两个以上就需要 @SecundaryTables 。
继承的映射:映射父类
不作为实体类的父类,需要标记为 MappedSuperclass
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class DogFather {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
import javax.persistence.*;
@Entity
@Table(name = "DOG")
public class Dog extends DogFather {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String color;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
解释:
- 有了 @MappedSuperclass 的 DogFather 类,使得其子类所继承的属性也能与数据库中的字段对应起来。
- 但 DogFather 并非实体,没有 id ,也没有对应的表。
- MappedSuperclass 可以是抽象类也可以是具体类。
对于 MappedSuperclass 的使用建议:
- 因为不作为实体,绝不能使用 @Entity 或 @Table
- 建议做成抽象类,这样就无法直接使用了
何时使用呢?如果该父类不需要用来查询,则可以将其标记为 MappedSuperclass。反之,则使用实体继承(见下文)。
继承的映射:单表
Single Table 策略就是在一个表中体现继承:
import javax.persistence.*;
@Entity
@Table(name = "DOG")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DOG_CLASS_NAME")
public abstract class Dog {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
// get and set
}
import javax.persistence.*;
@Entity
@DiscriminatorValue("SMALL_DOG")
public class SmallDog extends Dog {
private String littleBark;
public String getLittleBark() {
return littleBark;
}
public void setLittleBark(String littleBark) {
this.littleBark = littleBark;
}
}
import javax.persistence.*;
@Entity
@DiscriminatorValue("HUGE_DOG")
public class HugeDog extends Dog {
private int hugePooWeight;
public int getHugePooWeight() {
return hugePooWeight;
}
public void setHugePooWeight(int hugePooWeight) {
this.hugePooWeight = hugePooWeight;
}
}
解释:
- 注解 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 要放在父类
- @DiscriminatorColumn(name = “DOG_CLASS_NAME”) 指定了表中用于区分不同子类的列的列名
- @DiscriminatorValue 是“区分列”中存放的“区分值”
- 注意 id 在父类中,子类不允许声明 id
区分列的类型也可以定为整数:
- @DiscriminatorColumn(name = “DOG_CLASS_NAME”, discriminatorType = DiscriminatorType.INTEGER)
- @DiscriminatorValue("1")
继承的映射:连接
每个实体都有对应的表,而不是一个表:
import javax.persistence.*;
@Entity
@Table(name = "DOG")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DOG_CLASS_NAME")
public abstract class Dog {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
// get and set
}
import javax.persistence.*;
@Entity
@DiscriminatorValue("HUGE_DOG")
public class HugeDog extends Dog {
private int hugePooWeight;
public int getHugePooWeight() {
return hugePooWeight;
}
public void setHugePooWeight(int hugePooWeight) {
this.hugePooWeight = hugePooWeight;
}
}
import javax.persistence.*;
@Entity
@DiscriminatorValue("SMALL_DOG")
public class SmallDog extends Dog {
private String littleBark;
public String getLittleBark() {
return littleBark;
}
public void setLittleBark(String littleBark) {
this.littleBark = littleBark;
}
}
解释:
Dog 表
HugeDog 表
SmallDog 表
不管是否抽象类,都会有单独的表与之对应。
继承的映射:每个具体类都有一个表
具体类的表中会有继承而得的列
import javax.persistence.*;
@Entity
@Table(name = "DOG")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Dog {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
// get and set
}
import javax.persistence.Entity;
@Entity
public class HugeDog extends Dog {
private int hugePooWeight;
public int getHugePooWeight() {
return hugePooWeight;
}
public void setHugePooWeight(int hugePooWeight) {
this.hugePooWeight = hugePooWeight;
}
}
import javax.persistence.Entity;
@Entity
public class SmallDog extends Dog {
private String littleBark;
public String getLittleBark() {
return littleBark;
}
public void setLittleBark(String littleBark) {
this.littleBark = littleBark;
}
}
解释:
- 有了 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 之后,就不需要 @DiscriminatorColumn 和 @DiscriminatorValue 了
HugeDog
SmallDog
各种继承映射方式的优缺
他们各有优缺,采取哪种实现方式,要按实际应用而异,并没有最好的方法。
策略 | 优点 | 缺点 |
SINGLE_TABLE |
|
|
JOINED |
|
|
TABLE_PER_CLASS |
|
|
嵌入对象
当一张表中包含了完全不同类别的数据时,可以使用嵌入对象去管理。试想有个表包含了“人”和相应的“住址”:
使用嵌入对象来实现:
import javax.persistence.*;
@Embeddable
public class Address {
@Column(name = "house_address")
private String address;
@Column(name = "house_color")
private String color;
@Column(name = "house_number")
private int number;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
// get and set
}
import javax.persistence.*;
@Entity
@Table(name = "person")
public class Person {
@Id
private int id;
private String name;
private int age;
@Embedded
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
// get and set
}
解释:
- @Embeddable 注解使该类能被嵌入到其他实体类中。但注意 Address 类不是实体,它只是帮助管理数据而已。
- @Column注解用于属性与字段对应。
- @Embedded 注解使得 Address 的属性也能为 Person 所用。
- Address 也能为其他实体类所用。有很多方法可以在运行时重写 @Column。
元素集合 - 如何把一系列的值映射到一个类中
有时需要将一系列非实体的内容匹配到一个实体中。例如,人有很多邮件,狗有很多昵称。
代码演示:
import java.util.List;
import java.util.Set;
import javax.persistence.*;
@Entity
@Table(name = "person")
public class Person {
@Id
@GeneratedValue
private int id;
private String name;
@ElementCollection
@CollectionTable(name = "person_has_emails")
private Set<String> emails;
@ElementCollection(targetClass = CarBrands.class)
@Enumerated(EnumType.STRING)
private List<CarBrands> brands;
// get and set
}
public enum CarBrands {
FORD, FIAT, SUZUKI
}
解释:
- @ElementCollection 只能用于一般的属性(如 String、Enum)
- @Enumerated(EnumType.STRING) 只能在 @ElementCollection 出现是才能使用,他可以定义枚举值以什么类型保存的数据库中(详情)
- @CollectionTable(name = “person_has_emails”) 定义了存放该系列值的表的表名。如果想 brands 那样不指定,那么表明会被默认为 person_brands。
单向一对一与双向一对一
单向
试想每人只有一个电话,人知道自己用哪台手机,但手机不知道自己被谁使用:
Person 类定义如下:
import javax.persistence.*;
@Entity
public class Person {
@Id
@GeneratedValue
private int id;
private String name;
@OneToOne
@JoinColumn(name = "cellular_id")
private Cellular cellular;
// get and set
}
import javax.persistence.*;
@Entity
public class Cellular {
@Id
@GeneratedValue
private int id;
private int number;
// get and set
}
解释:
- 单向的话,只能做到 person.getCellular() ,而不能做到 cellular.getPerson() 。
- @OneToOne 表明了两个实体是一对一关系。
- @JoinColumn 使 Person 表中拥有 Cellular 的外键,表明 Person 拥有 Cellular。
双向
要使单向双向,只需这样改:
import javax.persistence.*;
@Entity
public class Cellular {
@Id
@GeneratedValue
private int id;
private int number;
@OneToOne(mappedBy = "cellular")
private Person person;
// get and set
}
解释:
- Callular 类也加入了 Person 属性,并标记 @OneToOne。
- mappedBy 表明“Person 拥有 Cellular”,并且外键在 person 表而不在cellular 表。
最佳的做法是只让其中一个实体作为“所有者”。也就是,从表中的 @OneToOne 需要加上 mappedBy。
单向和双向的一对多或多对一
就像一次只能呼叫一个电话,但一个电话可被呼叫多次。
先看多对一:
import javax.persistence.*;
@Entity
public class Call {
@Id
@GeneratedValue
private int id;
@ManyToOne
@JoinColumn(name = "cellular_id")
private Cellular cellular;
private long duration;
// get and set
}
解释:
- @ManyToOne 表明多个 Call 对应 一个 Cellular。
- @JoinColumn 定义了由 Call 来维护关系。
- @ManyToOne 所在的实体就是“所有者”,无法使用 mappedBy 来将其当做从表。
想创造双向关系,就需要改变 Cellular 类:
import javax.persistence.*;
@Entity
public class Cellular {
@Id
@GeneratedValue
private int id;
@OneToOne(mappedBy = "cellular")
private Person person;
@OneToMany(mappedBy = "cellular")
private List<Call> calls;
private int number;
// get and set
}
解释:
- @OneToMany 必须注解在集合属性之上。
- mappedBy 定义了 Call 是所有者。
所谓“所有者”,就是其表中有外键,它由 @JoinColumn 维护。
单向和双向的多对多
想像一个家庭的多个人养了多只狗,这些狗属于这家庭的多个人。
这种情况需要一个额外的表来保存两组实体的 id 的对应关系。
person 表:
dog 表:
person_dog 表:
Person 实体类:
import java.util.List;
import javax.persistence.*;
@Entity
public class Person {
@Id
@GeneratedValue
private int id;
private String name;
@ManyToMany
@JoinTable(name = "person_dog",
joinColumns = @JoinColumn(name = "person_id"),
inverseJoinColumns = @JoinColumn(name = "dog_id")
)
private List<Dog> dogs;
@OneToOne
@JoinColumn(name = "cellular_id")
private Cellular cellular;
// get and set
}
解释:
@JoinTable 设定关系表。name 是表名,joinColumn 是所有者,inverseJoinColumns 是被拥有者。
现在,再将人狗变成双向关系。
import java.util.List;
import javax.persistence.*;
@Entity
public class Dog {
@Id
@GeneratedValue
private int id;
private String name;
@ManyToMany(mappedBy = "dogs")
private List<Person> persons;
// get and set
}
这里加入了 Person 列表,并标注了 @ManyToMany 和 mappedBy (使得 Person 是所有者)。
注意,mappedBy 的值是所有者的属性名,而非其实体类名。
有额外字段的多对多
继续人狗的多对多关系,但现在每次有人收养狗的时候,都需要记录收养时间。明显这个时间应该存放于关系表中,而非 Person 或 Dog 中。
在此我们可以使用“关联实体”来实现。
下图展示了这个实体与人狗的关系:
要与人狗映射的话,先把代码改成以下这样:
import java.util.List;
import javax.persistence.*;
@Entity
public class Person {
@Id
@GeneratedValue
private int id;
private String name;
@OneToMany(mappedBy = "person")
private List<PersonDog> dogs;
// get and set
}
import java.util.List;
import javax.persistence.*;
@Entity
public class Dog {
@Id
@GeneratedValue
private int id;
private String name;
@OneToMany(mappedBy = "dog")
private List<PersonDog> persons;
// get and set
}
注意以上代码使用的是 @OneToMany 和 mappedBy 来描述人狗关系。现在不再使用 @ManyToMany ,但多了的 PersonDog 实体来将人狗连接起来。
import java.util.Date;
import javax.persistence.*;
@Entity
@IdClass(PersonDogId.class)
public class PersonDog {
@Id
@ManyToOne
@JoinColumn(name = "person_id")
private Person person;
@Id
@ManyToOne
@JoinColumn(name = "dog_id")
private Dog dog;
@Temporal(TemporalType.DATE)
private Date adoptionDate;
// get and set
}
从以上代码可以看到 PersonDog, Dog 和 Person 三者关系,以及一个额外的 adoptionDate 属性。另外,还使用了一个叫做 PersonDogId 的 IdClass :
import java.io.Serializable;
public class PersonDogId implements Serializable {
private static final long serialVersionUID = 1L;
private int person;
private int dog;
public int getPerson() {
return person;
}
public void setPerson(int person) {
this.person = person;
}
public int getDog() {
return dog;
}
public void setDog(int dog) {
this.dog = dog;
}
@Override
public int hashCode() {
return person + dog;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PersonDogId) {
PersonDogId personDogId = (PersonDogId) obj;
return personDogId.dog == dog && personDogId.person == person;
}
return false;
}
}
注意,PersonDogId 和 PersonDog 里的 person 和 dog 属性需要同名。如要使用复杂组合键,可参考前面章节。
级联是如何实现的? 该如何使用orphanremoval? 处理org.hibernate.TransientObjectException
在同一个事务中修改多的实体是很平常的。当为 person 添加 car 和 address 的时候,该 car 和 address 也是必须同时添加到它们对应的表中的。
以下的代码会引起 TransientObjectException :
EntityManager entityManager = // get a valid entity manager
Car car = new Car();
car.setName("Black Thunder");
Address address = new Address();
address.setName("Street A");
entityManager.getTransaction().begin();
Person person = entityManager.find(Person.class, 33);
person.setCar(car);
person.setAddress(address);
entityManager.getTransaction().commit();
entityManager.close();
如果是使用 EclipseLink JPA,则会报这样的错:
Caused by: java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST
transient 的实体是什么?not marked cascade PERSIST 的 relationship 又是什么?
JPA 需要清楚事务中所有的创建、修改、删除实体的来龙去脉。当事务开始时,所有从数据库取出的实体都是“与数据库关联的,并由 JPA 管理的”。
看看以下代码:
entityManager.getTransaction().begin();
Car myCar = entityManager.find(Car.class, 33);
myCar.setColor(Color.RED);
entityManager. getTransaction().commit();
虽然没有明显的 update 语句,但 myCar 的 color 就是会被更新并持久化,因为它是与数据库“关联的”,与 JPA 上下文“关联的”。所有“关联的”实体都会在 commit() 或调用 flush 后被持久化。
再看前面所说到的问题,以下代码:
entityManager.getTransaction().begin();
Person newPerson = new Person();
newPerson.setName("Mary");
Car myCar = entityManager.find(Car.class, 33);
myCar.setOwner(newPerson);
entityManager. getTransaction().commit();
myCar 和 newPerson 之间是有关系了,但问题是,newPerson 不是从数据库取出的,它在 JPA 上下文之外,是 JPA 不能管理的。
为了解决这种问题,JPA 提供“级联”的选项,它能在@OneToOne,@OneToMany 和 @ManyToMany 中使用。javax.persistence.CascadeType 枚举了所有可用的选项:
- CascadeType.DETACH
- CascadeType.MERGE
- CascadeType.PERSIST
- CascadeType.REFRESH
- CascadeType.REMOVE
- CascadeType.ALL
“级联”会按照你所定义的级联类型,做出相应的动作。看以下代码:
import javax.persistence.*;
@Entity
public class Car {
@Id
@GeneratedValue
private int id;
private String name;
@OneToOne(cascade = CascadeType.PERSIST)
private Person person;
// get and set
}
以上注解会使每次执行 entityManager.persist(car) 之时,同时执行 person 的持久化。
每个级联选项的解释:
选项 | 动作 | 触发时机 |
CascadeType.DETACH | 级联地失联 | JPA 上下文结束或者执行 entityManager.detach(),entityManager.clear() 之时 |
CascadeType.PERSIST | 级联 insert | 事务结束或者执行 entityManager.persist() 之时 |
CascadeType.MERGE | 级联 update | 有实体被更新,并事务结束或者执行 entityManager.merge() 之时 |
CascadeType.REMOVE | 级联 delete | 执行 entityManager.remove() 之时 |
CascadeType.REFRESH | 级联 select | 执行 entityManager.refresh() 之时 |
CascadeType.ALL | 以上所有级联 | 以上所有时机 |
建议:
- 使用 CascadeType.ALL 要谨慎,因为它包含了级联删除。
- 级联会有额外的性能开销。
- 可以使用 getReference() 减少开销。
触发级联的正确方法是,对含有cascade注解属性的实体,执行持久化。
import javax.persistence.*;
@Entity
public class Car {
@Id
@GeneratedValue
private int id;
private String name;
@OneToOne(cascade = CascadeType.PERSIST)
private Person person;
// get and set
}
import javax.persistence.*;
@Entity
public class Person {
@Id
private int id;
private String name;
@OneToOne(mappedBy = "person")
private Car car;
// get and set
}
对于以上两个实体类,只有这样才能触发级联:
entityManager.persist(car);
OrphanRemoval
看以下代码:
import javax.persistence.*;
@Entity
public class Address {
@Id
@GeneratedValue
private int id;
private String name;
// get and set
}
import javax.persistence.*;
@Entity
public class Person {
@Id
private int id;
private String name;
@OneToMany(orphanRemoval = true)
private List<Address> address;
// get and set
}
试想一种情况:只能通过 person 获取 address。
OrphanRemoval 的做法很像 CascadeType.REMOVE 。不同的是,当两个实体之间失去关系是,OrphanRemoval 就会做出级联删除。
person.setAddress(null);
例如执行以上代码,会导致原本相关的 address 实体成为孤儿,而 OrphanRemoval 则会将其级联删除。
OrphanRemoval 只能用于 @OneToOne 和 @OneToMany 之中。
一个应用一个实体管理器工厂
加载一个实体管理器工厂是耗费性能的,JPA 需要分析数据库,验证实体,还有其他琐碎的任务,所以,通常的做法是,一个应用只有一个 EntityManagerFactory。
以下代码就是这种做法:
import javax.persistence.*;
public abstract class ConnectionFactory {
private ConnectionFactory() {
}
private static EntityManagerFactory entityManagerFactory;
public static EntityManager getEntityManager() {
if (entityManagerFactory == null) {
entityManagerFactory = Persistence.createEntityManagerFactory("MyPersistenceUnit");
}
return entityManagerFactory.createEntityManager();
}
}
Lazy/Eager 的工作方式
一个实体可能会有一些容量很大的属性:
import javax.persistence.*;
@Entity
public class Car {
@Id
@GeneratedValue
private int id;
private String name;
@ManyToOne
private Person person;
// get and set
}
import java.util.List;
import javax.persistence.*;
@Entity
public class Person {
@Id
private int id;
private String name;
@OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
private List<Car> cars;
@Lob
@Basic(fetch = FetchType.LAZY)
private Byte[] hugePicture;
// get and set
}
解释:
cars 集合和 hugePicture 数组都使用了 FetchType.LAZY
FetchType.LAZY 使得 entityManager.find(Person.class, person_id) 时,不会同时获取该属性的数据。这样便节省了带宽。
只有使用对应的 get 方法时,才发起另一个查询以获取数据。
没有任何注解的属性,默认是 FetchType.EAGER 。想改变的话就加上 @Basic(fetch = FetchType.LAZY) 。
”有关系“的属性,有各自默认的加载方式(当然也可以自定义来改变它):
- ONE结尾的,默认 FetchType.EAGER
- MANY结尾的,默认 FetchType.LAZY
使用 LAZY 的时候,有可能遭遇 Lazy Initialization Exception 。那是因为当真正想获取该 LAZY 属性时,数据库链接已被关闭。这里有四种解决方法。
处理cannot simultaneously fetch multiple bags
这个错误发生在,实体有多于一个集合类型的属性需要及时加载,之时。
import java.util.List;
import javax.persistence.*;
@Entity
public class Person {
@Id
private int id;
private String name;
@OneToMany(mappedBy = "person", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<Car> cars;
@OneToMany(fetch = FetchType.EAGER)
private List<Dog> dogs;
// get and set
}