JPA 详解

时间:2022-06-04
本文章向大家介绍JPA 详解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
简介

Java Persistence API(JPA)是将Java对象和关系型数据库对象映射起来规范。实现这个规范后开发者可以使用相同的代码可以在任意的数据库中执行CRUD操作,实现的框架不仅仅是处理和数据库交换的代码(JDBC),同时也会将数据库中的数据和Java对象映射起来,无需手动进行转换。此教程基于JAP2.1。 JPA 主要包含的组件:

  • 实体: 对于当前JPA的规范,实体就是POJO。
  • 对象关系信息:应用开发者必须提供数据库表数据和Java对象之间的对应关系
  • JPQL: JPA的目的是抽象具体的数据库,框架仍然提供了类SQL的方式处理特殊的方法
项目实战

首先创建项目:

mvn archetype:create -DgroupId=com.javacodegeeks.ultimate -DartifactId=jpa

添加依赖:

<properties>
    <jee.version>7.0</jee.version>
    <h2.version>1.3.176</h2.version>
    <hibernate.version>4.3.8.Final</hibernate.version>
</properties>

<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>${jee.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>${h2.version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>${hibernate.version}</version>
    </dependency>
</dependencies>
EntityManager 和 Persistence Unit

配置 persistence.xml信息,文件保存在src/main/resource/META-INF下。

<?xml version="1.0" encoding="UTF-8" ?>
<persistence 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
 http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="PersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="connection.driver_class" value="org.h2.Driver"/>
            <property name="hibernate.connection.url" value="jdbc:h2:~/jpa"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

事务类型设置的是RESOURCE_LOCAL,表示事务由应用自己控制,如果使用了容器提供的事务可以设置为JTA。 provider 设置为org.hibernate.ejb.HibernatePersistence 表示使用Hibernate实现的JPA。 之后的设置就是设置JPA连接数据库的基本信息。

public class Main {
    private static final Logger LOGGER = Logger.getLogger("JPA");

    public static void main(String[] args) {
        Main main = new Main();
        main.run();
    }

    public void run() {
        EntityManagerFactory factory = null;
        EntityManager entityManager = null;
        try {
            factory = Persistence.createEntityManagerFactory("PersistenceUnit");
            entityManager = factory.createEntityManager();
            persistPerson(entityManager);
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            e.printStackTrace();
        } finally {
            if (entityManager != null) {
                entityManager.close();
            }
            if (factory != null) {
                factory.close();
            }
        }
    }
    ...

JPA中所有的操作基本都由EntityManager完成。为了获取EntityManger,需要创建EntityManagerFactory实例。一个应用尽需要一个EntityManagerFactory。

事务

现在来实现上面代码的persistPersion()方法,以为我们选择的是事务类型是本地事务,所有事务要有应用控制,存储一个对象

private void persistPerson(EntityManager entityManager) {
    EntityTransaction transaction = entityManager.getTransaction();
    try {
        transaction.begin();
        Person person = new Person();
        person.setFirstName("Homer");
        person.setLastName("Simpson");
        entityManager.persist(person);
        transaction.commit();
    } catch (Exception e) {
        if (transaction.isActive()) {
            transaction.rollback();
        }
    }
}

上述的Person对象的定义:表名为T_PERSION

@Entity
@Table(name = "T_PERSON")
public class Person {
    private Long id;
    private String firstName;
    private String lastName;

    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Column(name = "FIRST_NAME")
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    @Column(name = "LAST_NAME")
    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

如果表明和类名相同,那么@Table是可以省略的,但是仍然推荐加上。 注解@Column 是用来映射Java对象和表中的列的,及时不加注解,JPA仍然会映射,除非其使用注解@Transient修饰,则不会被映射。关于@Column的使用

@Colunm(name="FIRST_NAME", length=100, nullable = false, unique = false)

上述注解的意思就是映射表中列名为FIRST_NAME的列,长度100字符,不能空,不唯一,当试图插入null值是会抛出异常并会滚事务。 对于@Id和@GeneratedValue是告诉JAP,这个值是主键并且其值是由数据库自动生成的。 上述例子,@Column是修饰getter的同样可以直接修饰字段。

@Entity
@Table(name = "T_PERSON")
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "FIRST_NAME")
    private String firstName;
    @Column(name = "LAST_NAME")
    private String lastName;
    ...

这两种方法几乎没什么区别,唯一的区别在于如果注解修饰字段子类无法重写其注解。 另一个需要注意的是需要在一个实体的层次上使用一种注解方式。可以在JPA的整个项目混用注解字段或者方法,但是在一个实体和它的子类中需要确保使用的是同一种注解方式。如果要修改子类的注解方式,可以使用 @Access注解改变

@Entity
@Table(name = "T_GEEK")
@Access(AccessType.PROPERTY)
public class Geek extends Person {
...
继承

对于Geek类,其直接继承与Person

@Entity
@Table(name = "T_GEEK")
public class Geek extends Person {
    private String favouriteProgrammingLanguage;
    private List<Project> projects = new ArrayList<Project>();

    @Column(name = "FAV_PROG_LANG")
    public String getFavouriteProgrammingLanguage() {
            return favouriteProgrammingLanguage;
    }

    public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
        this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
    }
    ...
}

Hibernate 会重新创建T_PERSON表

Hibernate: create table T_PERSON (DTYPE varchar(31) not null, id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (id))

保存Geek

private void persistGeek(EntityManager entityManager) {
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    Geek geek = new Geek();
    geek.setFirstName("Gavin");
    geek.setLastName("Coffee");
    geek.setFavouriteProgrammingLanguage("Java");
    entityManager.persist(geek);
    geek = new Geek();
    geek.setFirstName("Thomas");
    geek.setLastName("Micro");
    geek.setFavouriteProgrammingLanguage("C#");
    entityManager.persist(geek);
    geek = new Geek();
    geek.setFirstName("Christian");
    geek.setLastName("Cup");
    geek.setFavouriteProgrammingLanguage("Java");
    entityManager.persist(geek);
    transaction.commit();
}

T_PERSION保存的数据

sql> select * from t_person;
DTYPE  | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
Person | 1  | Homer      | Simpson   | null
Geek   | 2  | Gavin      | Coffee    | Java
Geek   | 3  | Thomas     | Micro     | C#
Geek   | 4  | Christian  | Cup       | Java

DTYPE表示了不同类型的PERSON,对于Person,其FAV_PROG_LANG是null。 如果不喜欢使用字符串类型的鉴别器,可以设置@DiscriminatorColumn来表示不同的类型

@DiscriminatroColumn(name="PERSION_TYPE",discriminatorType=DiscriminatroType.INTEGER)

存储结果:

sql> select * from t_person;
PERSON_TYPE | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
-1907849355 | 1  | Homer      | Simpson   | null
2215460     | 2  | Gavin      | Coffee    | Java
2215460     | 3  | Thomas     | Micro     | C#
2215460     | 4  | Christian  | Cup       | Java

并不是所有情况下都想把信息存储在一张表中,可以使用@Inheritance选择不同的存储策略,对于这种一共有三种选择:

  • SINGLE_TABLE: 这个策略就是把所有的字段映射到一张表中
  • JOINERD: 对每个实体创建一张表。每个表只包含其映射的对象的信息,加载一个实体的时候,通过join的方式获取所有的信息,虽然降低了存储空间,但是
  • TABLE_PER_CLASS: 所有的表中都会包含全部信息。 使用JOIN的方式: @Inheritance(strategy=InheritanceType.JOINED) Hibernate会创建两张表: Hibernate: create table T_GEEK (FAV_PROG_LANG varchar(255), id bigint not null, primary key (id)) Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id)) 其获取值: sql> select * from t_person; ID | FIRST_NAME | LAST_NAME 1 | Homer | Simpson 2 | Gavin | Coffee 3 | Thomas | Micro 4 | Christian | Cup (4 rows, 12 ms) sql> select * from t_geek; FAV_PROG_LANG | ID Java | 2 C# | 3 Java | 4 (3 rows, 7 ms) 使用SQL获取完整的值: select person0_.id as id1_2_, person0_.FIRST_NAME as FIRST_NA2_2_, person0_.LAST_NAME as LAST_NAM3_2_, person0_1_.FAV_PROG_LANG as FAV_PROG1_1_, case when person0_1_.id is not null then 1 when person0_.id is not null then 0 end as clazz_ from T_PERSON person0_ left outer join T_GEEK person0_1_ on person0_.id=person0_1_.id 使用JPA实现如下: TypedQuery<Person> query = entityManager.createQuery("from Person", Person.class); List<Person> resultList = query.getResultList(); for (Person person : resultList) { StringBuilder sb = new StringBuilder(); sb.append(person.getFirstName()).append(" ").append(person.getLastName()); if (person instanceof Geek) { Geek geek = (Geek)person; sb.append(" ").append(geek.getFavouriteProgrammingLanguage()); } LOGGER.info(sb.toString()); } 关系 表,实体之间的关系主要有,1对1,1对多,多对多,嵌入,集合。 1对1 @Entity @Table(name = "T_ID_CARD") public class IdCard { private Long id; private String idNumber; private Date issueDate; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Column(name = "ID_NUMBER") public String getIdNumber() { return idNumber; } public void setIdNumber(String idNumber) { this.idNumber = idNumber; } @Column(name = "ISSUE_DATE") @Temporal(TemporalType.TIMESTAMP) public Date getIssueDate() { return issueDate; } public void setIssueDate(Date issueDate) { this.issueDate = issueDate; } } @Temporal告诉JPA如何将其序列化保存到数据库中,可以选择DATE,TIME,TIMESTAMP 然后设置JPA,每个Persion都有一个IdCard @Entity @Table(name = "T_PERSON") public class Person { ... private IdCard idCard; ... @OneToOne @JoinColumn(name = "ID_CARD_ID") public IdCard getIdCard() { return idCard; } 创建表的语句: ```SQL create table T_ID_CARD ( id bigint generated by default as identity, ID_NUMBER varchar(255), ISSUE_DATE timestamp, primary key (id) )

create table T_PERSON ( id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), ID_CARD_ID bigint, primary key (id) )

@OneToOne有一个重要特性就是可以设置何时获取IdCard。
```Java
@OneToOne(fetch = FetchType.EAGER)

默认值EAGER表示每次获取Person都要获取IdCard

@OneToOne(fetch = FetchType.LAZY)

表示只有在需要IdCard的时候才会去获取 其获取SQL如下:

Hibernate: 
    select
        person0_.id as id1_3_,
        person0_.FIRST_NAME as FIRST_NA2_3_,
        person0_.ID_CARD_ID as ID_CARD_4_3_,
        person0_.LAST_NAME as LAST_NAM3_3_,
        person0_1_.FAV_PROG_LANG as FAV_PROG1_1_,
        case 
            when person0_1_.id is not null then 1 
            when person0_.id is not null then 0 
        end as clazz_ 
    from
        T_PERSON person0_ 
    left outer join
        T_GEEK person0_1_ 
            on person0_.id=person0_1_.id
Hibernate: 
    select
        idcard0_.id as id1_2_0_,
        idcard0_.ID_NUMBER as ID_NUMBE2_2_0_,
        idcard0_.ISSUE_DATE as ISSUE_DA3_2_0_ 
    from
        T_ID_CARD idcard0_ 
    where
        idcard0_.id=?
1对多
@Entity
@Table(name = "T_PHONE")
public class Phone {
    private Long id;
    private String number;
    private Person person;

    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Column(name = "NUMBER")
    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PERSON_ID")
    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }
}

对于Person

private List<Phone> phones = new ArrayList<>();
...
@OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
public List<Phone> getPhones() {
    return phones;
}

@OneToMany 的mappedBy 表示使用Phone中的person字段来关联。其fetch设置为LAZY模式,因为每次获取Person的时候不一定需要其Phone,如果需要可以通过如下方式获取:

TypedQuery<Person> query = entityManager.createQuery("from Person p left join fetch p.phones", Person.class);

对应的SQL如下:

select
    person0_.id as id1_3_0_,
    phones1_.id as id1_4_1_,
    person0_.FIRST_NAME as FIRST_NA2_3_0_,
    person0_.ID_CARD_ID as ID_CARD_4_3_0_,
    person0_.LAST_NAME as LAST_NAM3_3_0_,
    person0_1_.FAV_PROG_LANG as FAV_PROG1_1_0_,
    case 
        when person0_1_.id is not null then 1 
        when person0_.id is not null then 0 
    end as clazz_0_,
    phones1_.NUMBER as NUMBER2_4_1_,
    phones1_.PERSON_ID as PERSON_I3_4_1_,
    phones1_.PERSON_ID as PERSON_I3_3_0__,
    phones1_.id as id1_4_0__ 
from
    T_PERSON person0_ 
left outer join
    T_GEEK person0_1_ 
        on person0_.id=person0_1_.id 
left outer join
    T_PHONE phones1_ 
        on person0_.id=phones1_.PERSON_ID
多对多
@Entity
@Table(name = "T_PROJECT")
public class Project {
    private Long id;
    private String title;
    private List<Geek> geeks = new ArrayList<Geek>();

    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Column(name = "TITLE")
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @ManyToMany(mappedBy="projects")
    public List<Geek> getGeeks() {
        return geeks;
    }

    public void setGeeks(List<Geek> geeks) {
        this.geeks = geeks;
    }
}

通过@ManyToMany中的mappedBy得知,需要在Geek中的字段projects做多对多关系 Geek类:

private List<Project> projects = new ArrayList<>();
...
@ManyToMany
@JoinTable(
        name="T_GEEK_PROJECT",
        joinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")},
        inverseJoinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")})
public List<Project> getProjects() {
    return projects;
}

在Geek类中提供了@JoinTable明确其和哪个表做join,joinColunms和inverseJoinConlumns则表示如何做JOIN。 如果是在Project中实现是同样的,只需要将joinColumns和inverseJoinColumn换一下即可 Project类

@ManyToMany
@JoinTable(
        name="T_GEEK_PROJECT",
        joinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")},
        inverseJoinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")})
public List<Geek> getGeeks() {
    return geeks;
}

Geek类:

@ManyToMany(mappedBy="geeks")
public List<Project> getProjects() {
    return projects;
}

获取如下:

List<Geek> resultList = entityManager.createQuery("from Geek g where g.favouriteProgrammingLanguage = :fpl", Geek.class).setParameter("fpl", "Java").getResultList();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
Project project = new Project();
project.setTitle("Java Project");
for (Geek geek : resultList) {
    project.getGeeks().add(geek);
    geek.getProjects().add(project);
}
entityManager.persist(project);
transaction.commit();
嵌入元素/嵌入的集合

如果想要在Java中更加细粒度的控制其model可以使用嵌入模式。例如Project有startDate 和 endDate,可以创建Period类以便重用开始和结束时间。

@Embeddable
public class Period {
    private Date startDate;
    private Date endDate;

    @Column(name ="START_DATE")
    public Date getStartDate() {
        return startDate;
    }

    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

    @Column(name ="END_DATE")
    public Date getEndDate() {
        return endDate;
    }

    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }
}

在Project中加入:

private Period projectPeriod;

@Embedded
public Period getProjectPeriod() {
    return projectPeriod;
}

public void setProjectPeriod(Period projectPeriod) {
    this.projectPeriod = projectPeriod;
}

Project表结构:

create table T_PROJECT (
    id bigint generated by default as identity,
    END_DATE timestamp,
    START_DATE timestamp,
    projectType varchar(255),
    TITLE varchar(255),
    primary key (id)
)

查询:

entityManager.createQuery("from Project p where p.projectPeriod.startDate = :startDate", Project.class).setParameter("startDate", createDate(1, 1, 2015));
数据类型和转换

SQL类型

java类型

VARCHAR (CHAR, VARCHAR2, CLOB, TEXT)

String (char, char[])

NUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE)

Number (BigDecimal, BigInteger, Integer, Double, Long, Float, Short, Byte)

NUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE)

int, long, float, double, short, byte

VARBINARY (BINARY, BLOB)

byte[]

BOOLEAN (BIT, SMALLINT, INT, NUMBER)

boolean(Boolean)

TIMESTAMP (DATE, DATETIME)

java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar

NUMERIC (VARCHAR, CHAR)

java.lang.Enum

VARBINARY (BINARY, BLOB)

java.util.Serializable

例如使用enum

@Entity
@Table(name = "T_PROJECT")
public class Project {
...
    private ProjectType projectType;

    public enum ProjectType {
        FIXED, TIME_AND_MATERIAL
    }
    ...
    @Enumerated(EnumType.ORDINAL)
    public ProjectType getProjectType() {
        return projectType;
    }

    public void setProjectType(ProjectType projectType) {
        this.projectType = projectType;
    }
}

通过使用@Enumerated 将enum和数据库的字段进行映射,EnumType.ORDINAL 表示使用数字表示enum并保存到数据中。

sql> select * from t_project;
ID | PROJECTTYPE | TITLE
1  | 1           | Java Project
(1 row, 2 ms)

如果使用 EnumType.String 则表示使用String保存,调用enum的方法name()获取其名字。

sql> select * from t_project;
ID | PROJECTTYPE       | TITLE
1  | TIME_AND_MATERIAL | Java Project
(1 row, 2 ms)

如果没有满足需要的转换器,可以自己构建

@Converter
public class BooleanConverter implements AttributeConverter<Boolean, Integer> {

    @Override
    public Integer convertToDatabaseColumn(Boolean aBoolean) {
        if (Boolean.TRUE.equals(aBoolean)) {
            return 1;
        } else {
            return -1;
        }
    }

    @Override
    public Boolean convertToEntityAttribute(Integer value) {
        if (value == null) {
            return Boolean.FALSE;
        } else {
            if (value == 1) {
                return Boolean.TRUE;
            } else {
                return Boolean.FALSE;
            }
        }
    }
}

使用

private boolean valid;
...
@Column(name = "VALID")
@Convert(converter = BooleanConverter.class)
public boolean isValid() {
    return valid;
}

public void setValid(boolean valid) {
    this.valid = valid;
}

结果如下:

sql> select * from t_id_card;
ID | ID_NUMBER | ISSUE_DATE              | VALID
1  | 4711      | 2015-02-04 16:43:30.233 | -1
标准API

目前为止一直使用JPQL来查询数据库,现在通过使用JPQL提供的标准的API来查询。 例如:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> personRoot = query.from(Person.class);
query.where(builder.equal(personRoot.get("firstName"), "Homer"));
List<Person> resultList = entityManager.createQuery(query).getResultList();

首先通过EntityManger创建CriteriaBuilder。通过CruiteriaBuilder创建CriteriaQuery。提供一个函数获取where语句

query.where(builder.equal(personRoot.get("firstName"), "Homer"));

对于多where语句

query.where(builder.and(
    builder.equal(personRoot.get("firstName"), "Homer"), 
    builder.equal(personRoot.get("lastName"), "Simpson")));

CriteriaQuery定义了一下子句和选项:

  • distinct() 清除重复
  • from() 设置查询的表
  • select() 表示select语句
  • multiselect() select的列表
  • where() where子句
  • orderBy() 确定排序顺序
  • groupBy() 分组结果
  • having() having子句
  • subquery() 子查询
序列

对于@GeneratedValue提供了三种策略:

  • TABLE: JPA创建另一个表来提供序号
  • SEQUENCE: 如果数据库支持SEQUENCE,使用SEQUENCE方式创建序号
  • IDENTITY: 数据库提供标识列使用标识列提供序号

例如使用TABLE

@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "TABLE_GENERATOR")
@TableGenerator(name = "TABLE_GENERATOR", table="T_SEQUENCES", pkColumnName = "SEQ_NAME", valueColumnName = "SEQ_VALUE", pkColumnValue = "PHONE")
public Long getId() {
    return id;
}

SQL

sql> select * from t_sequences;
SEQ_NAME | SEQ_VALUE
PHONE    | 1

使用SEQUENCE

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "S_PROJECT")
@SequenceGenerator(name = "S_PROJECT", sequenceName = "S_PROJECT", allocationSize = 100)
public Long getId() {
    return id;
}

通过@SequenceGenerator JPA获取其SEQUENCE,名字为S_PROJECT,分配的大小(100)。 使用IDENTITY

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
    return id;
}

表:

create table T_ID_CARD (
    id bigint generated by default as identity,
    ID_NUMBER varchar(255),
    ISSUE_DATE timestamp,
    VALID integer,
    primary key (id)
)