jpa mini book

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


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 方法

查询实体也如 @IdClass 那样。


复杂组合键


复杂组合键由其他实体组成,不只是普通的 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
  • 只需一个表
  • 容易理解其模型
  • 查询方便
  • 高性能
  • 列要允许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 {

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











  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值