jpa mini book

来源:互联网 发布:日本手机直播软件 编辑:程序博客网 时间:2024/05/22 09:38

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;@Entitypublic 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;@Entitypublic 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.*;@Entitypublic 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 @GeneratedValueprivate 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 managerCarId carId = new CarId(33, "Ford");Car persistedCar = em.find(Car.class, carId);System.out.println(persistedCar.getName() + " - " + persistedCar.getSerial());

@Embeddable


另一种定义简单组合键的方法如下:


import javax.persistence.*;@Entitypublic 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;@Embeddablepublic 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.*;@Entitypublic 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.*;@Entitypublic 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.*;@Entitypublic 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 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;@MappedSuperclasspublic 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;@Entitypublic class HugeDog extends Dog {    private int hugePooWeight;    public int getHugePooWeight() {        return hugePooWeight;    }    public void setHugePooWeight(int hugePooWeight) {        this.hugePooWeight = hugePooWeight;    }}

import javax.persistence.Entity;@Entitypublic 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.*;@Embeddablepublic 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.*;@Entitypublic class Person {    @Id    @GeneratedValue    private int id;        private String name;        @OneToOne    @JoinColumn(name = "cellular_id")    private Cellular cellular;        // get and set}

import javax.persistence.*;@Entitypublic 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.*;@Entitypublic 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.*;@Entitypublic 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.*;@Entitypublic 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.*;@Entitypublic 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.*;@Entitypublic 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.*;@Entitypublic 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.*;@Entitypublic 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 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.*;@Entitypublic 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.*;@Entitypublic class Car {    @Id    @GeneratedValue    private int id;        private String name;        @OneToOne(cascade = CascadeType.PERSIST)    private Person person;        // get and set}

import javax.persistence.*;@Entitypublic 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.*;@Entitypublic class Address {    @Id    @GeneratedValue    private int id;        private String name;        // get and set}

import javax.persistence.*;@Entitypublic 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.*;@Entitypublic class Car {    @Id    @GeneratedValue    private int id;        private String name;        @ManyToOne    private Person person;        // get and set}


import java.util.List;import javax.persistence.*;@Entitypublic 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.*;@Entitypublic 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
原创粉丝点击