欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JPA快速入门(一)

程序员文章站 2022-07-14 23:26:55
...

JPA简介

JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

Sun引入新的JPA ORM规范出于两个原因:
其一,简化现有Java EE和Java SE应用开发工作;
其二,Sun希望整合ORM技术,实现天下归一。

JPA的宗旨是为POJO提供持久化标准规范,由此可见,经过这几年的实践探索,能够脱离容器独立运行,方便开发和测试的理念已经深入人心了。Hibernate3.2+、TopLink 10.1.3以及OpenJPA都提供了JPA的实现。

JPA的总体思想和现有Hibernate、TopLink、JDO等ORM框架大体一致。总的来说,JPA包括以下3方面的技术:

ORM映射元数据

JPA支持XML和JDK5.0注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中;

API

用来操作实体对象,执行CRUD操作,框架在后台替代我们完成所有的事情,开发者从繁琐的JDBC和SQL代码中解脱出来。

查询语言

这是持久化操作中很重要的一个方面,通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。
JPA快速入门(一)

JPA开发环境搭建

  • jar包的依赖
    如果是maven项目,将下面的配置添加到pom.xml文件中

    <build>
      <plugins>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>3.3</version>
              <configuration>
                  <target>1.8</target>
                  <source>1.8</source>
                  <encoding>UTF-8</encoding>
              </configuration>
          </plugin>
      </plugins>
    </build>
    <dependencies>
      <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-core</artifactId>
          <version>4.3.5.Final</version>
      </dependency>
      <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-entitymanager</artifactId>
          <version>4.3.5.Final</version>
      </dependency>
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.21</version>
      </dependency>
      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
      </dependency>
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.16.6</version>
      </dependency>
    </dependencies>
    

    如果是普通的java项目,将下面的jar包添加到项目的lib目录中
    JPA快速入门(一)

  • persistence.xml文件
    如果是maven项目,在src/main/resources下创建META-INF文件夹,将persistence.xml文件放在该目录下
    如果是普通的java项目,在src下创建META-INF文件夹,将persistence.xml文件夹放在该目录下

在persistence.xml文件中做如下配置

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
    <!--
    JPA根据下面的配置信息创建EntityManagerFactory,一个项目中可以配置多个持久单元
    name:为当前持久单元命名,可以通过该名称指定加载对应的配置信息
-->
    <persistence-unit name="myPersistence">
        <!--指定扫描贴Entity实体类所在的jar包-->
        <properties>
    <!--数据库的方言,告诉JPA当前应用使用的数据库-->
            <property name="hibernate.dialect"     value="org.hibernate.dialect.MySQL5Dialect"/>
            <!--jpa的相关的配置信息-->
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql:///jpa"/>
            <property name="javax.persistence.jdbc.driver"     value="com.mysql.jdbc.Driver"/>
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value="admin"/>
    <!--是否在控制台打印执行的sql语句-->
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

到此,开发JPA应用的环境就搭建完成,接下来,在此基础上来完成基本的CRUD操作吧

基于JPA的CRUD

//JPA会扫描到贴了Entity注解的类,将其作为需要持久化的类
@Entity
//根据需求,对类和表做相关映射(如:表名)
@Table(name=”user”)
public class User {
//标识该字段为主键列对应的字段
@Id
//指定主键的生成策略
@GeneratedValue(strategy = GenerationType.AUTO)
//为当前字段和对应的列做映射(如:列名,列的长度等)
@Column(name = “id”)
private Long id;
@Column(name = “name”,length = 20)
private String name;
@Column(name = “sn”,nullable = false)
private String sn;
//对日期类型做映射
@Temporal(TemporalType.DATE)
private Date hiredate;
}


- EntityManagerFactory和EntityManager对象的创建
1. EntityManagerFactory:JPA通过加载META-INF/persistence.xml文件中配置的persistence-unit创建EntityManagerFactory对象,该对象相当于一个连接池对象,用来创建EntityManager,是线程安全的,多线程可以共用同一个EntityManagerFactory,创建该对象需要消耗较多的资源,所以通常一个项目只需要创建一个EntityManagerFactory对象
2. EntityManager:相当于一个连接对象,该对象线程不安全,所以,每次对象数据库的访问应该创建一个新的EntityManager对象

public class JPAUtil {

private static EntityManagerFactory emf;

private JPAUtil() {}

static {
    //加载persistence.xml文件中的persistence-util中的配置信息创建EntityManagerFactory对象
    emf = Persistence.createEntityManagerFactory("myPersistence");
}

//使用EntityManager创建EntityManager对象
public static EntityManager getEntityManager() {
    return emf.createEntityManager();
}

}


- 保存操作

@Test
public void testSave() throws Exception {
//封装需要持久化的数据
User u = new User();
u.setName(“Neld”);
u.setSn(“sn”);
u.setHiredate(new Date());

EntityManager em = JPAUtil.getEntityManager();
//开启事务
em.getTransaction().begin();
//执行保存
em.persist(u);
//提交事务
em.getTransaction().commit();
//释放资源
em.close();

}

- 删除操作

@Test
public void testDelete() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
em.getTransaction().begin();
User u = em.getReference(User.class, 1L);
//执行删除,将持久化状态的对象从数据库中删除
em.remove(u);
em.getTransaction().commit();
em.close();
}

- 修改操作

@Test
public void testUpdate() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
em.getTransaction().begin();
User u = em.find(User.class, 1L);
u.setName(“xxxx”);
em.merge(u);
em.getTransaction().commit();
em.close();
}

- 查询操作

@Test
public void testGet() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
//查询指定类型和OID的用户信息
User u = em.find(User.class, 1L);
em.close();
System.out.println(u);
}

- CRUD小结
1. persistence.xml文件的配置
配置连接数据库的基本信息
JPA的基本行为配置
2. 实体类的基本映射
@Entity:标注该类为持久化类
JPA扫描到类上的注解,会将当前类作为持久化类
@Table:配置当前类和表的相关映射
下面的注解可以贴在字段或者是get方法上,
如果选定了一个位置,那么所有的属性相关的注解都应该贴在这个位置,意思是说,不能一部分在字段上,一部分在get方法上
@Id:主键属性的映射---和表中的主键映射
@GeneratedValue:主键生成策略(指定生成主键的方式:自增长/手动设置)
@Column:配置当前属性和列的映射
@Temporal:对日期类型的属性映射(Date/DateTime/TimeStemp)

3. 完成CRUD的步骤
加载persistence.xml文件,使用指定的<persistence-unit>配置创建EntityManagerFactory对象,相当于根据配置信息创建一个连接池对象
创建EntityManager对象,相当于获取到一个连接对象
开启事务
执行crud相关的方法(persist/merge/remove/find),查询所有调用Query中的getResultList方法
Persist:保存数据
Merge:保存或者更新,当对象有OID的时候,更新,反之,保存
Remove:删除数据
Find:根据主键查询数据
Query:其他的查询需要使用该对象,传入对应的JPQL(相当于SQL),调用getResultList方法执行查询,返回对应的List集合
提交事务
释放资源

####hbm2ddl工具的使用
在持久层应用的开发过程中,我们发现,实体类和表结构是一一对应的,所以,我们会想,是否可以让JPA根据实体类和对应的映射信息的配置,为我们自动的生成对应的表结构呢?

答案是肯定的,又因为我们现在讲的是hibernate对JPA的实现,所以我们应用hibernate中提供的hbm2ddl工具来实现,配置很简单,在persistence.xml文件中作如下配置即可

<property name="hibernate.hbm2ddl.auto" value="create"/>

接下来,我们来解释一下每种策略的含义及使用场景

- hibernate.hbm2ddl.auto=create
    在启动的时候先删除被管理的实体对应的表,然后再创建jpa管理的实体类对应的表
- hibernate.hbm2ddl.auto=create-drop
    和create一致,只是在关闭系统之前会删除jpa管理的所有的表
- hibernate.hbm2ddl.auto=update
    在启动的时候,检查实体类和表结构是否有变化,如果有,执行更新表结构相关的sql
如果添加一个属性,JPA可以帮我们在表中添加对应的列
如果删除一个属性,JPA不会帮我们去表中删除对应的列
如果修改一个属性(类型),JPA不会帮我们去表中删除对应的列
- hibernate.hbm2ddl.auto=validate
    在启动的时候,检查实体类和表结构是否有变化,如果有,启动失败,抛出异常
Caused by: org.hibernate.HibernateException: Missing column: sn in jpa.user

#####选择:
- 在开发阶段,我们通常使用create或者create-drop,可以快速的创建对应的表结构
- 在测试阶段,不要使用create或者create-drop,因为这样会将我们辛苦录入的测试数据删除,所以,我们使用update,在实体类修改的时候,更新表结构即可
- 在生产环境中,我们通常使用validate,这样可以在启动阶段发现表结构相关的问题,至于表结构的修改,交给我们的DBA去完成吧.

####单对象映射中常用的注解
- 对象映射相关
1. @Entity:
对实体类的映射,默认使用当前类的简单名称作为类名,如在使用JPQL做查询的时候,使用该名字实现数据的查询
JPQL语句:SELECT u FROM User u;
User:为默认使用的类名,可以通过Entity中的name属性修改
@Entity(name=”UserInfo”):将类的名称修改为UserInfo,那么上面的JPQL中的User修改为UserInfo即可

2. @Table:
指定实体类映射的表的相关信息,如:表名,默认和类名一致
@Table(name=”t_user”):将映射的表名修改为t_user

3. persistence.xml文件中的相关元素的配置说明
<class>:指定需要扫描的实体类
<exclude-unlisted-classes>:设置为true的时候,表示不扫描这里没有列出来的类
<jar-file>:指定对项目中引入的jar包中的类进行扫描

- 属性相关:
1. @GeneratedValue,主键生成策略
在一张表中,主键列的信息通常需要受到程序员的特殊关照,这里我们需要探讨一下主键的生成方式(自动生成/手动设值)
首先,我们需要在主键属性上使用@GeneratedValue注解中的strategy属性来设值主键的生成方式

    1. strategy=GenerationType.AUTO
    把主键生成策略交给JPA厂商(Persistence Provider),由它根据具体的数据库选择合适的策略,可以是Table/Sequence/Identity中的一种。假如数据库是Oracle,则选择Sequence。
    如果不做特别指定,默认是使用这种方式生成主键

    2. strategy=GenerationType.IDENTITY
    多数数据库支持IDENTITY,数据库会在新行插入时自动给ID赋值,这也叫做ID自增长列,比如MySQL中可以在创建表时声明“AUTO_INCREMENT”,该策略在Oracle数据库中不支持

    3. strategy=GenerationType.TABLE
    有时候为了不依赖于数据库的具体实现,在不同数据库之间更好的移植,可以在数据库中新建序列表来生成主键,序列表一般包含两个字段:第一个字段引用不 同的关系表,第二个字段是该关系表的最大序号。这样,只需要一张序列就可以用于多张表的主键生成。
    如果不指定表生成器,JPA厂商会使用默认的表,比如Hibernate在Oracle数据库上会默认使用表hibernate_sequence。
这种方式虽然通用性最好,所有的关系型数据库都支持,但是由于不能充分利用具体数据库的特性,建议不要优先使用。

    4. strategy=GenerationType.SEQUENCE
    Oracle不支持ID自增长列而是使用序列的机制生成主键ID,对此,可以选用序列作为主键生成策略:
    如果不指定序列生成器的名称,则使用厂商提供的默认序列生成器,比如Hibernate默认提供的序列名称为hibernate_sequence。
支持的数据库: Oracle、PostgreSQL、DB2
属性映射
@Column:
使用该注解可以对属性和列进行相关映射

该注解可以贴在字段上,也可贴在getter方法上,但是必须是统一的,不能一部分在字段上,一部分在getter方法上

- @Access
在实际开发中,也可以告诉JPA只去扫描哪个位置上的@Column注解,如果没有就不在去其他地方扫描
@Access(AccessType.PROPERTY):属性,对应着get方法
@Access(AccessType.FIELD):字段:对应字段

- @Column
name:列名,通常,属性名和列名一直的时候,不需要指定,默认使用属性名作为列名
unique:唯一性约束
nullable:非空约束
insertable:false,表示在生成insert语句的时候不插入这一列的值
updatable:false,表示在生成update语句的时候不更新这一列的值
length:指定该列的长度
columnDefination:自定义列的类型,默认是JPA根据属性的类型自动生成
precision:在使用decimal类型的时候指定总长度
scale:在使用decimal类型的时候指定小数位数

- @Temporal:
日期类型的映射
指定日期类型的属性对应的列的类型(date/datatime/timestamp)

- @Transient:
非持久化类型的映射
JPA在做对象关系映射的时候,默认是对实体类中的所有属性进行映射的,如果有不需要映射的属性,可以使用该注解完成

- @Lob:
大数据类型的映射
对象如果是String类型的,默认情况下载表中映射的是VARCHAR类型
该注解可以对应text/blob/clob类型进行映射,如:

@Lob
private String content;

####一级缓存
在EntityManager中存在一个缓存区域,称之为一级缓存

在该缓存区中,会将查询到的对象缓存到该区域中

如果在同一个EntityManager中,查询相同OID的数据,那么只需要发送一条sql

在事务提交/关闭EntityManager之后,一级缓存会清空,所以在不同的EntityManager中使用不
同的一级缓存

一级缓存也可以使用下面的方法手动清除缓存数据
detach:清除一级缓存中指定的对象
clear:清除一级缓存中的所有的缓存数据

![image.png](https://upload-images.jianshu.io/upload_images/11387429-acc5a9d5ff1ac1d5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

但是一级缓存的缓存能力是非常有限的,因为我们不会经常在一个EntityManager中查询相同的数据
延迟加载

JPA中,根据主键查询数据可以使用下面两个方法完成:
<T> T find(Class<T> type, Object oid);
<T> T getReference(Class<T> type, Object oid);
相同点:都是根据主键查询指定类型的数据

不同点: getReference方法是在真实使用该对象的时候才会发送查询的sql语句,如

public void testGetReference() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
//这里不会立即发送sql查询
User u = em.getReference(User.class, 1L);
System.out.println(“——————-“);
//在访问User对象中的属性值的时候表示真正使用该对象
System.out.println(u.getName());
em.close();
}

执行结果:

Hibernate: select user0.id as id1_0_0, user0.hiredate as hiredate2_0_0, user0.name as name3_0_0, user0.sn as sn4_0_0 from User user0 where user0.id=?

Neld

根据执行的打印结果可以看到,是我们在真正使用该对象的时候才会执行查询的sql,而在这之前是不会发送SQL执行数据的查询

####延迟加载
getReference方法查询数据的方式我们称之为延迟加载

什么是延迟加载? 就是不会立即执行查询的sql,而是延迟到真正使用的时候再执行,上面的例子已经证明了这一点

再观察:
find方法查询到的结果,如果查询到了对应的数据,返回查询到的结果即可,反之,返回null,所以可以使用ifnull判断是否有数据
getReference方法查询到的结果,无论是否查询到了数据,结果都不会是null,所以不能使用ifnull判断是否有对应的数据
如果在表中没有对应的数据,抛出异常
javax.persistence.EntityNotFoundException: Unable to find cn.wolfcode._01_hello.User with id 2

原理:
JPA使用动态代理机制实现延迟加载,覆写该对象中的所有的getter方法,在getter方法中执行查询当前对象的sql

延迟加载需要搞懂的问题:
1.延迟加载什么时候发送SQL执行数据?
2.为什么需要在关闭EntityManager对象之前初始化延迟加载对象?
3.为什么在访问对象的get方法的时候,会去初始化当前对象(发送SQL执行查询)呢?
4.使用find方法没有查询到数据的时候,返回值是什么?使用getReference方法没有查询到数据的时候,返回值是什么?


####对象状态
对象的状态是JPA中非常重要的概念,描述了实体对象从瞬时到持久、从删除到游离的状态变换。对实体的操作其实就是对象实体状态的改变, 这对于我们分析SQL的执行情况有很大的帮助。
- 瞬时状态(Transient)
使用new关键字创建出来的新对象,没有OID,不在一级缓存中
- 持久状态(Persistent)
调用持久化方法之后,将对象保存到数据库中,对象状态转化成持久状态
- 游离状态(Detached)
对象存在于数据库中,但是不在一级缓存中
- 删除状态(Removed)
事务一旦提交,对象就会被从数据库中删除,是介于持久状态和被删除之间的一个临界状态

我们可以通过下面的表格了解到各个状态的特点:

状态 | 是否在一级缓存 | 是否有OID
---|---|---
瞬时状态(Transient)|    否|    否
持久状态(Persistent)|    是|    是
游离状态(Detached)|    否|    是
删除状态(Removed)|    是|    是

EntityManager提供一系列的方法管理实体对象的状态,包括:

- persist, 将新创建的或已删除的实体转变为Persistent状态,数据存入数据库。
- remove,删除持久状态的实体
- merge,将游离实体转变为Persistent状态,数据存入数据库。

如果使用了事务管理,则事务的commit/rollback也会改变实体的状态。
如图:
![image.png](https://upload-images.jianshu.io/upload_images/11387429-1c342c0586756090.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
有了对对象状态的了解之后,我们来分析面的案例中sql的发送

@Test
public void test() throws Exception{
EntityManager em = JPAUtil.getEntityManager();
em.getTransaction().begin();
//通过find方法查询到处于持久状态的User对象
User u = em.find(User.class, 1L);
u.setName(“Lucy”);//①
em.getTransaction().commit();
em.close();
}
```

执行结果:
Hibernate: select user0.id as id1_0_0, user0.hiredate as hiredate2_0_0, user0.name as name3_0_0, user0.sn as sn4_0_0 from User user0 where user0.id=?

Hibernate: update User set hiredate=?, name=?, sn=? where id=?

  • 分析:
    ①:在这里,我们修改了查询出来处于持久状态的User对象的name属性的值

我们并没有调用merge方法去更新User对象,为什么会发送update语句呢?

  • 原因:
    首先,将数据从数据库中查询出来后,在内存中会有两份数据,一份在EntityManager一级缓存区域,一份在EntityManager的快照区,两份数据完全一样

然后,修改User的name属性时,其实是修改的缓存区的数据

最后,在提交事务的时候,会清理一级缓存,此时会对比两份数据是否一致,如果不一致,发送对应的update语句将缓存中的脏数据(和数据库中的数据不一致)同步到数据库中

所以,在上面的例子中,我们看到执行了一条更新语句,这样相信大家就能够理解了,这也是在我们了解了对象的状态之后对SQL的发送有了更深入的认识

JPA快速入门(一)