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 connectionStatement 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 查询例子在此。

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@Idprivate 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;@OneToManyprivate 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 {@Idprivate int serial;@Idprivate 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 construcotpublic 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@Overridepublic int hashCode() {return serial + brand.hashCode();}// Must have an equals method@Overridepublic 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 {@EmbeddedIdprivate 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 construcotpublic 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@Overridepublic int hashCode() {return serial + brand.hashCode();}// Must have an equals method@Overridepublic 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 方法

查询实体也如 @IdClass 那样。

复杂组合键

复杂组合键由其他实体组成,不只是普通的 java 自带类型。

想想狗屋这个实体以狗来作为 id :

import javax.persistence.*;@Entity
public class Dog {@Idprivate 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 {@Idprivate 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;}@Overridepublic int hashCode() {return person + dog;}@Overridepublic 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 somethingentityManager.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
  • 只需一个表
  • 容易理解其模型
  • 查询方便
  • 高性能
  • 列要允许null值
JOINED
  • 遵循面向对象的原则
  • 需 insert 的表会最多,最耗性能
TABLE_PER_CLASS
  • 仅查询一个子类时,性能高
  • 查询多个类时,需要union或者多条查询语句,降低性能

嵌入对象

当一张表中包含了完全不同类别的数据时,可以使用嵌入对象去管理。试想有个表包含了“人”和相应的“住址”:

使用嵌入对象来实现:

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 {@Idprivate int id;private String name;private int age;@Embeddedprivate 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@GeneratedValueprivate 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@GeneratedValueprivate int id;private String name;@OneToOne@JoinColumn(name = "cellular_id")private Cellular cellular;// get and set
}
import javax.persistence.*;@Entity
public class Cellular {@Id@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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@GeneratedValueprivate 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;}@Overridepublic int hashCode() {return person + dog;}@Overridepublic 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 managerCar 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@GeneratedValueprivate 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@GeneratedValueprivate int id;private String name;@OneToOne(cascade = CascadeType.PERSIST)private Person person;// get and set
}
import javax.persistence.*;@Entity
public class Person {@Idprivate 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@GeneratedValueprivate int id;private String name;    // get and set
}
import javax.persistence.*;@Entity
public class Person {@Idprivate 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@GeneratedValueprivate int id;private String name;@ManyToOneprivate Person person;    // get and set
}
import java.util.List;
import javax.persistence.*;@Entity
public class Person {@Idprivate 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 {@Idprivate 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
}

jpa mini book相关推荐

  1. JPA不同包下同类名查询出错

    不同包下同类名查询出错 异常现象:使用JPA进行查询时,JPA的实体类映射到了另外一个包下的同名类,由于两个同名类中字段名和字段数不相同,所以会出现种种查询错误. 原因及对策:这里应该是jpa实体类默 ...

  2. MybatisPlus忽略实体类中的非数据库字段、JPA忽略实体类中的非数据库字段、HeHibernate忽略实体类中的非数据库字段

    mybatis plus忽略映射字段时可以在实体类属性上使用以下注解: @TableField(exist = false):表示该属性不为数据库表字段,但又是必须使用的. @TableField(e ...

  3. 使用JPA进行Update操作 @Query注解的用法,JPL

    使用jpa进行update操作有两种,第一种就是先查询,set,再进行save更新.这种做法过于繁杂,我只是要进行一个更新操作却变成了三步,所以我推荐使用第二种: @Modifying @Query( ...

  4. 使用JPA进行update操作时,报org.springframework.beans.factory.BeanCreationException: Error creating bean with

    使用JPA进行update操作时,报org.springframework.beans.factory.BeanCreationException: Error creating bean with ...

  5. Spring Boot整合Spring Data JPA操作数据

    一. Sping Data JPA 简介 Spring Data JPA 是 Spring 基于 ORM 框架.JPA 规范的基础上封装的一套 JPA 应用框架,底层使用了 Hibernate 的 J ...

  6. Spring Data JPA 五分钟快速入门和实践

    Spring Data JPA(类似于Java Web 中的 DAO) 操作声明持久层的接口(Repository) 三个核心接口: CrudRepository PagingAndSortingRe ...

  7. [JAVA EE] JPA 查询用法:自定义查询,分页查询

    项目已上传:https://codechina.csdn.net/qq_36286039/javaee 自定义查询 问题:内置的crud功能不满足需求时如何添加自定义查询? 几种自定义查询方法 方法命 ...

  8. [JAVA EE] JPA 技术实践:完成增、删、改、查操作

    项目已上传:https://codechina.csdn.net/qq_36286039/javaee 注意本项目是包含后文JPA 查询用法内容的,若您想看只有本文内容的项目,请下载: https:/ ...

  9. [JAVA EE] JPA技术基础:完成数据列表的删除

    接上一篇:[JAVA EE] JPA技术基础:完成数据列表显示 本章完成数据列表的删除 修改 UserController.java package com.example.demo.controll ...

最新文章

  1. 摄像头模组(CCM)与镀膜
  2. switch()中不允许的数据类型有?
  3. 【完结】总结12大CNN主流模型架构设计思想
  4. 用tensorflow还原PSENet网络
  5. 计算机网络之数据链路层:15、以太网、适配器、MAC地址
  6. python爬取NBA湖人队球星的数据,并且用Excel保存
  7. 若依如何配置允许跨域访问?
  8. Python菜鸟入门:day12编程学习
  9. Java中使用开源库JSoup解析HTML文件实例
  10. python基础笔记(六)_数据清洗及建模
  11. 笔记-JavaScript高级程序设计-第六章思维导图
  12. IPFS(DRAFT 3) 中文版白皮书
  13. 学习笔记:微波遥感反演土壤水分——理论模型和经验模型(1)
  14. python 加速运算
  15. 路飞学城Python-Day37(practise)
  16. 安科瑞智能照明控制系统在医院行业的应用
  17. 让华为P30运行如飞的,是这个叫方舟的……
  18. ENVI5.3安装或卸载时提示错误1628代码
  19. android手机如何加速,小技巧:如何给Android手机上的Chrome浏览器加速
  20. 有4个圆塔、圆心分别为(2,2)、(-2,2)、(-2,-2)、(2,-2),圆半径为1,见图4.5。这4个塔的高度为10m,塔以外无建筑物。今输入任一点的坐标,求该点的建筑高度(塔外的高度为零)

热门文章

  1. 最新网站被挂马被跳转解决办法
  2. 8051单片机外扩ROM
  3. 2022-2028年中国汽车样车试制行业市场调研分析及发展规模预测报告
  4. 小胖虎带你了解MySQL 算术运算符和逻辑运算符
  5. 暗备用的运行状态_厂用电的备用电源有明备用与暗备用两种,它们有什么区别?...
  6. 工业机器人为什么需要机器视觉
  7. PHP 服务器端什么时候设置 Set-Cookie
  8. 2022年A特种设备相关管理(电梯)操作证考试题库及答案
  9. 2018年计算机网络考研题目
  10. 关于Java中的iml文件的分析和理解