JPA规范

  • Post author:
  • Post category:其他

[TOC]

ORM思想

ORM全称Object Relational Mapping,即对象关系映射,是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。

通俗点讲,用来把对象映射到基于sql的关系模型数据库结构中去。这样,我们在具体的操作实体对象的时候,就不需要再去和复杂的sql语句打交道,只需简单的操作实体对象的属性和方法。ORM技术是在对象和关系之间提供了一条桥梁,前台的对象型数据和数据库中的关系型的数据通过这个桥梁来相互转化 。

JPA规范

JPA(Java持久化API)是一种Java应用程序接口规范,描述java应用中关系数据的管理,充当面向对象的领域模型和关系数据库系统之间的桥梁。

Application code —> JPA —->实现JPA规范的框架(比如hibernate、spring data jpa) —-> 数据库

graph LR

A[Application code] --> |jpa规范| B(hibernate)
B -->|jdbc| C(mysql数据库)

hibernate框架

hibernate是一个开源的对象关系映射框架,它对jdbc进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成sql语句,自动执行,使得java程序员可以随心所欲的使用对象编程思维来操纵数据库。

入门案例

1、创建项目工程,导入相关依赖

    compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.4.3.Final'
    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'
    compile group: 'org.hibernate', name: 'hibernate-c3p0', version: '5.4.3.Final'

2、配置jpa的核心配置文件

  • 配置到路径下的一个叫做META-INF的文件夹下
  • 文件名字必须为 persistence.xml
    • 必须配置persistence-unit(持久化单元)节点
    • name:持久化单元名称,自定义
    • transaction-type:事务管理的方式
      • JTA:分布式事务管理
      • RESOURCE_LOCAL:本地事务管理
    • 配置jpa的实现方式
    • 配置实体类
    • 配置数据库信息
    • 配置jpa实现方的配置信息(可选)
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
    <!--必须配置persistence-unit节点-->
    <persistence-unit name="myJpa" transaction-type="RESOURCE_LOCAL">
        <!--jpa实现方式-->
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <!--配置实体类-->
        <class>com.lxf.User</class>
        <properties>
            <!--数据库信息-->
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value="crystal1024"/>
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/study?serverTimezone=GMT"/>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>

            <!--jpa实现方的配置信息-->
            <!--日志显示sql语句-->
            <property name="hibernate.show_sql" value="true"/>
            <!--
            自动创建数据库的方式:
                create:程序运行时创建数据库表,如果表存在,先删除再创建
                update:程序运行时创建数据库表,如果表存在,不会创建,表有改动的话会更新
                none:不会创建表
            -->
            <property name="hibernate.hbm2ddl.auto" value="update"/>
        </properties>
    </persistence-unit>
</persistence>

3、编写实体类POJO

public class User {
    private Integer id;
    private String name;
    private Integer age;
    private Integer sex;//0未知1男2女
    private String address;
    private String phone;
    ...省略getter setter
}

4、配置实体类和表,类中属性和表中字段的映射关系

  • 类与表的映射关系
    • @Entity:声明实体类
    • @Table:配置实体类与表的映射关系
      • name:配置数据库表的名称
  • 属性与表字段的映射关系
    • @Id:声明主键的配置
    • @GeneratedValue:配置主键的生成策略
      • GenerationType.IDENTITY:自增,使用底层数据库支持的自动增长方式对id自增(比如mysql)
      • GenerationType.SEQUENCE:序列,(比如oracle)
      • GenerationType.TABLE:jpa提供的一种机制,通过一张数据库表的形式帮助我们完成主键自增
      • GenerationType.AUTO:由程序自动的帮助我们选择主键生成策略
    • @Column:配置属性与字段的映射关系
      • name:数据库中表字段的名字
import javax.persistence.*;

@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;
    @Column(name = "name")
    private String name;
    @Column(name = "age")
    private Integer age;
    @Column(name = "sex")
    private Integer sex;//0未知1男2女
    @Column(name = "address")
    private String address;
    @Column(name = "phone")
    private String phone;
    ...省略getter setter
}

5、jpa操作数据库

  • 加载配置文件,创建工厂(实体管理类工厂)对象。
    • Persistence:用于创建实体管理器工厂(EntityManagerFactory)
      • createEntityManagerFactory:根据持久化单元名称创建实体管理器工厂
    • EntityManagerFactory:用于创建实体管理器对象(EntityManager),创建比较浪费资源,线程安全对象。所以一般会以静态代码块的形式创建一个公共的EntityManagerFactory对象。
      • createEntityManager:内部维护了数据库信息、缓存、实体管理器对象等很多信息
  • 获取实体管理器
    • EntityManager:实体管理器。
      • getTransaction:创建事务对象
      • persist:保存
      • merge:更新
      • remove:删除
      • find/getRefrence:根据id查询
  • 获取事务对象,开启事务
    • EntityTransaction:事务对象。
      • begin:开启事务
      • commit:提交事务
      • rollback:回滚事务
  • 完成CRUD操作
  • 提交事务(异常时回滚事务)
  • 释放资源
    @Test
    public void testSave(){
        //1.加载配置文件,获取工厂对象
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myJpa");
        //2.创建实体管理器
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        //3.获取事务对象
        EntityTransaction transaction = entityManager.getTransaction();
        //4.开启事务
        transaction.begin();
        try {
            //5.相关CRUD操作
            User user = new User();
            user.setName("tom");
            user.setAge(18);
            user.setSex(1);
            //保存
            entityManager.persist(user);
            //6.提交事务
            transaction.commit();
        }catch (Exception e){
            //6.回滚事务
            transaction.rollback();
            throw new RuntimeException("出异常啦");
        }finally {
            //7.释放资源
            entityManager.close();
            entityManagerFactory.close();
        }
    }

控制台打印出的sql日志:

Hibernate: create table user (id integer not null auto_increment, address varchar(255), age integer, name varchar(255), phone varchar(255), sex integer, primary key (id)) engine=InnoDB

Hibernate: insert into user (address, age, name, phone, sex) values (?, ?, ?, ?, ?)

优化EntityManagerFactory的创建

上面有提到,EntityManagerFactory的创建比较浪费资源,而且它是线程安全对象。所以一般会以静态代码块的形式创建一个公共的EntityManagerFactory对象。

public class JPAUtil {
    private static EntityManagerFactory factory;
    
    static {
        factory = Persistence.createEntityManagerFactory("myJpa");
    }
    
    public static EntityManager getEntityManager(){
        return factory.createEntityManager();
    }
}

此时我们的代码会变成这样:

    @Test
    public void testSave(){
        EntityManager entityManager = JPAUtil.getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            User user = new User();
            user.setName("tom");
            user.setAge(18);
            user.setSex(1);
            entityManager.persist(user);
            transaction.commit();
        }catch (Exception e){
            transaction.rollback();
            throw new RuntimeException("出异常啦");
        }finally {
            entityManager.close();
        }
    }

数据库操作

插入操作

User user = new User();
user.setName("tom");
user.setAge(18);
user.setSex(1);
//接收要插入的对象
entityManager.persist(user);

删除操作

//先查询出我们要删除的对象
User user = entityManager.getReference(User.class, 1);
//接收要删除的对象
entityManager.remove(user);

更新操作

User user = entityManager.getReference(User.class, 2);
user.setName("lili");
//接收要修改的对象
entityManager.merge(user);

查询操作

内置API

根据主键查询单个:

 /**
  * 参数:
  *  1. class对象:查询数据结果需要包装的实体类类型的字节码
  *  2. 主键  这里是id
  */
 User user = entityManager.find(User.class, 1);

 /**
  * 参数:
  *  1. class对象:查询数据结果需要包装的实体类类型的字节码
  *  2. 主键  这里是id
  */
 User user = entityManager.getReference(User.class, 1);

find和getReference两者的区别:

  • find:立即加载
    • 获取的对象就是要查询的对象本身
    • 调用find方法时,会立即通过sql语句查询数据库
  • getReference:延迟加载(懒加载)
    • 获取的对象是一个动态代理对象
    • 调用getReference方法并不会立即通过sql语句查询数据库,而是当你使用这个对象的时候才会进行数据库查询

语句查询

创建查询对象Query

jpa提供了一系列create方法来获取一个Query对象进行复杂查询。

jpql全称Java Persistence Query Language,java持久化查询语言,它和sql的语法很像,不过操作的是类和属性。

  • createQuery:接收jpql语句
String jpql = "select u.name from User u";
//String jpql = "select name from User";//和上面语句效果是一样的
Query query = entityManager.createQuery(jpql);
  • createNamedQuery:执行命名查询,其实和createQuery是一样的,提前定义好的jpql语句
@Entity
@Table(name = "user")
@NamedQuery(name = "queryName",query = "select u.name from User u")
public class User {
    ...
}

Query query = entityManager.createNamedQuery("queryName");
  • createNativeQuery:接收原生sql语句
Query query = entityManager.createNativeQuery("select u.name from user as u");

常用查询操作

排序
  • sql:select * from user order by id desc
  • jpql:from User order by id desc
统计
  • sql:select count(id) from user
  • jpql:select count(id) from User
分页
  • sql:select * from user limit 1,10
  • jpql:from User
String jpql = "from User";
Query query = entityManager.createQuery(jpql);
query.setFirstResult(1);
query.setMaxResults(10);
条件查询
  • sql:select * from user where id > 1
  • jpql:from User where id > 1

Query的常用方法

  • executeUpdate:执行更新和删除操作。
  • getFirstResult:返回查询对象设置为检索的第一个定位结果。
  • getMaxResults:返回查询对象设置为检索的最大结果数。
  • getResultList:返回结果列表。
  • setFirstResult:分配要检索的第一个结果的位置。
  • setMaxResults:分配要检索的最大结果数。
  • setParameter:设置jpql语句中的占位符

Criteria查询

Criteria查询是在jpa 2.0的版本加入的一个更加符合面向对象思维的类型安全的查询方式。

首先介绍几个概念:

  • CriteriaBuilder:用于构造过滤条件(Predicate),内部提供了==大量的api==。在该接口中使用Predicate代替Expression <Boolean>可以解决java泛型向下兼容的问题。
  • CriteriaQuery<T>:定义顶级查询的功能。内部提供where、groupBy、orderby、having等方法。
  • Root<X>:根查询通常指向实体。

举个列子:select * from User where id > 3 and age >18

通俗点讲,select * from User属于Root,where属于CriteriaQuery,查询方式,id > 3 and age >18属于CriteriaBuilder构建出来的查询条件,这么讲可能不是很准确,但很容易理解。

简单翻译一下上面的语句:

public class JpaCriteriaTest {
    @Test
    public void testCriteria(){
        EntityManager entityManager = JPAUtil.getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            //拿到CriteriaBuilder、CriteriaQuery、Root三个对象
            CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            CriteriaQuery<User> criteriaQuery = criteriaBuilder.createQuery(User.class);
            Root<User> root = criteriaQuery.from(User.class);

            //构建过滤条件
            Predicate idCondition = criteriaBuilder.greaterThan(root.get("id"), 3);
            Predicate ageCondition = criteriaBuilder.greaterThan(root.get("age"), 18);
            Predicate predicate = criteriaBuilder.and(idCondition, ageCondition);

            //组合查询
            criteriaQuery.where(predicate);

            //查询并获取结果
            TypedQuery<User> typedQuery = entityManager.createQuery(criteriaQuery);
            List<User> resultList = typedQuery.getResultList();//得到id>3的所有数据
            resultList.forEach(new Consumer<User>() {
                @Override
                public void accept(User user) {
                    System.out.println(user);
                }
            });

            transaction.commit();
        }catch (Exception e){
            transaction.rollback();
            e.printStackTrace();
            throw new RuntimeException("出异常啦");
        }finally {
            entityManager.close();
        }
    }
}

如果我们只想查询某几个字段,可以使用CriteriaQuery<Tuple>:

//拿到CriteriaBuilder、CriteriaQuery、Root三个对象
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> tupleQuery = criteriaBuilder.createTupleQuery();
Root<User> root = tupleQuery.from(User.class);
tupleQuery.multiselect(root.get("name"),root.get("age"));

//构建过滤条件
Predicate idCondition = criteriaBuilder.greaterThan(root.get("id"), 3);
Predicate ageCondition = criteriaBuilder.greaterThan(root.get("age"), 18);
Predicate predicate = criteriaBuilder.and(idCondition, ageCondition);

//组合查询
tupleQuery.where(predicate);

//查询并获取结果
TypedQuery<Tuple> typedQuery = entityManager.createQuery(tupleQuery);
List<Tuple> resultList = typedQuery.getResultList();//得到id>3的所有数据
resultList.forEach(new Consumer<Tuple>() {
   @Override
   public void accept(Tuple tuple) {
      System.out.println(tuple.get(0,String.class));
       System.out.println(tuple.get(1,Integer.class));
   }
});

多表关系映射

一对多

  • 主表:一的一方为主表
  • 从表:多的一方为从表
  • 外键:需要再从从表上新建一列作为外键,它的取值来源于主表的主键

另外,一对一其实就是一种特殊的一对多。

案例

一个student表,一个school表,一个学校可以有很多学生,一个学生只属于一个学校。

  • 表关系:一对多
    • 主表:school
    • 从表:student,我们需要在从表上添加外键
  • 编写实体
    • School:除了自有属性外应该包含一个List<Student>。
    • Student:除了自有属性外应该有一个School属性,一个外键字段。
  • 使用JPA注解配置映射关系:一般一方映射,另一方参照即可
    • @OneToMany:声明一对多
      • targetEntity:目标实体(从表实体)
      • mappedBy:参照映射
    • @ManyToOne:声明多对一
      • targetEntity:目标实体(从表实体)
    • @JoinColumn:配置外键
      • name:从表上的外键字段名称
      • referencedColumnName:参照的主表主键字段名称
@Entity
@Table
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Integer studentId;
    @Column
    private String name;
    @Column
    private Double score;
    @Column
    private Integer sex;
    @Column(name = "school_id",insertable = false,updatable = false)//外键字段
    private Integer schoolId;

    @ManyToOne(targetEntity = School.class)//配置多对一关系
    @JoinColumn(name = "school_id",referencedColumnName = "schoolId")//配置外键
    private School school;
    ...
}

@Entity
@Table
public class School {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer schoolId;
    @Column
    private String name;
    @Column
    private String address;

//    @OneToMany(targetEntity = Student.class)//声明关系,一对多
//    @JoinColumn(name = "school_id",referencedColumnName = "schoolId")//配置外键
    @OneToMany(mappedBy = "school")//Student里面已经配置了映射关系,表示参照Student里面的school
    private List<Student> students = new ArrayList<>();
    ...
}

保存测试

public class JpaOneToManyTest {

    @Test
    public void testSave(){
        EntityManager entityManager = JPAUtil.getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            Student student = new Student();
            student.setName("石昊");
            School school = new School();
            school.setName("天神书院");

            //创建关系
            student.setSchool(school);
//            school.getStudents().add(student);//两句代码都可以创建关系

            entityManager.persist(school);//先保存主表,如果先保存从表的话最好需要多一个update操作设置从表外键字段值
            entityManager.persist(student);

            transaction.commit();
        }catch (Exception e){
            transaction.rollback();
            e.printStackTrace();
            throw new RuntimeException("出异常啦");
        }finally {
            entityManager.close();
        }
    }
}

删除测试

  • 主表配置外键映射:这种情况下删除会先将从表外键值设为null,再删除主表数据。
  • 主表放弃外键维护,参照从表的映射:这种情况下只能使用级联删除。

级联操作

操作一个对象的同时操作它的关联对象。(先操作关联对象)

我们使用cascade来配置级联关系:

  • CascadeType.ALL:级联所有操作
  • CascadeType.PERSIST:级联保存操作
  • CascadeType.MERGE:级联更新操作
  • CascadeType.REMOVE:级联删除操作
  • CascadeType.REFRESH:级联refresh操作
  • CascadeType.DETACH:级联detach操作
@ManyToOne(targetEntity = School.class,cascade = CascadeType.ALL)
@JoinColumn(name = "school_id",referencedColumnName = "schoolId")

级联保存

Student student = new Student();
student.setName("叶凡");
School school = new School();
school.setName("荒古禁地");

//创建关系
 student.setSchool(school);

//这里只保存从表,主表会被级联保存
entityManager.persist(school);

级联删除

@OneToMany(mappedBy = "school",cascade = CascadeType.ALL)
private List<Student> students = new ArrayList<>();
School school = entityManager.find(School.class, 10);
entityManager.remove(school);//此时主表和从表中相关的数据都会删除

多对多

案例

一个developer表,一个language表,一个开发者可以会多种语言,一种语言也会有很多开发者会。

中间表:中间表中最少应该由两个字段组成,这两个字段作为外键指向两张表的主键,又组成了联合主键。

  • 表关系:多对多
    • 开发者表
    • 语言表
    • 中间表:最少应该由两个字段组成,这两个字段作为外键指向两张表的主键,又组成了联合主键。
  • 编写实体
    • Develop:除了自有属性外应该包含一个List<Language>。
    • Language:除了自有属性外应该包含一个List<Develop>。
    • 中间表:可以建一个实体,也可以直接数据库建表,一般用不到这个实体。
  • 使用JPA注解配置映射关系:一般一方映射,另一方参照即可
    • @ManyToMany:声明多对多
      • targetEntity:目标实体
    • @JoinTable:添加中间表
      • name:中间表名
      • joinColumns:添加一个@JoinColumn数组,表示==当前对象==在中间表的外键
      • inverseJoinColumns:添加一个@JoinColumn数组,表示==对方对象==在中间表的外键
@Entity
@Table
public class Developer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Integer developerId;
    @Column
    private String name;

    @ManyToMany(targetEntity = Language.class)
    @JoinTable(name = "middle_develop_language",
            joinColumns = @JoinColumn(name = "develop_id", referencedColumnName = "developerId"),
            inverseJoinColumns = @JoinColumn(name = "language_id", referencedColumnName = "languageId"))
    private List<Language> languages = new ArrayList<>();
    ...
}


@Entity
@Table
public class Language {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Integer languageId;
    @Column
    private String des;

//    @ManyToMany(targetEntity = Developer.class)
//    @JoinTable(name = "middle_develop_language",
//            joinColumns = @JoinColumn(name = "language_id", referencedColumnName = "languageId"),
//            inverseJoinColumns = @JoinColumn(name = "develop_id", referencedColumnName = "developerId"))
    @ManyToMany(mappedBy = "languages")//放弃维护权,参照对方的映射关系
    private List<Developer> developers = new ArrayList<>();
    private List<Developer> developers = new ArrayList<>();
    ...
}

保存测试

public class JpaManyToManyTest {

    @Test
    public void testSave(){
        EntityManager entityManager = JPAUtil.getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            Developer developer = new Developer();
            developer.setName("lxf");
            Language language = new Language();
            language.setDes("java");

            //创建关系
            developer.getLanguages().add(language);

            entityManager.persist(developer);
            entityManager.persist(language);

            transaction.commit();
        }catch (Exception e){
            transaction.rollback();
            e.printStackTrace();
            throw new RuntimeException("出异常啦");
        }finally {
            entityManager.close();
        }
    }
}

级联操作

级联的配置和一对多是一样的,只需要在@ManyToMany配上cascade = CascadeType.ALL属性。

    @ManyToMany(targetEntity = Language.class,cascade = CascadeType.ALL)
    @JoinTable(name = "middle_develop_language",
            joinColumns = @JoinColumn(name = "develop_id", referencedColumnName = "developerId"),
            inverseJoinColumns = @JoinColumn(name = "language_id", referencedColumnName = "languageId"))
    private List<Language> languages = new ArrayList<>();

级联保存

Developer developer = new Developer();
developer.setName("lxf");
Language language = new Language();
language.setDes("java");

developer.getLanguages().add(language);

entityManager.persist(developer);

级联删除

Developer developer = entityManager.find(Developer.class, 1);
entityManager.remove(developer);//注意此时只能通过developer级联删除,因为只有Developer配置了cascade

对象导航查询

对象导航查询并不是一种新的查询方式,而是在多表关系中,通过查询某个对象,可以直接通过get得到相关联的数据信息,这是jpa的一种特性,但需要注意:

  • jpa默认使用的是懒加载的方式来获取相关信息,即首次查询时只查了单表信息,如果需要使用其相关信息,才会再次发送sql语句查询数据库。
  • 如果需要立即加载,即在一开始就直接查询关联表的所有信息,则需要在映射关系中配置fetch属性。
    • FetchType.EAGER:立即加载
    • FetchType.LAZY:懒加载
  • fetch的默认值:需要查多方的默认懒加载,需要查单方的默认立即加载
    • @OneToMany:默认FetchType.LAZY
    • @ManyToMany:默认FetchType.EAGER
    • @ManyToMany:默认FetchType.LAZY
    @OneToMany(mappedBy = "school",cascade = CascadeType.ALL,fetch = FetchType.EAGER)
    private List<Student> students = new ArrayList<>();

通过一方查多方

以上面的一对多案例为例,通过查询一个学校,得到该学校的所有学生。

public class JpaObjectQueryTest {
    @Test
    public void test(){
        EntityManager entityManager = JPAUtil.getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
//            School school = entityManager.find(School.class, 5);
            School school = entityManager.getReference(School.class, 5);
            //可以直接通过get方法来得到和其相关的信息,如果懒加载,则此时会发送sql语句查询关联表信息
            List<Student> students = school.getStudents();
            System.out.println(students.size());
            students.forEach(new Consumer<Student>() {
                @Override
                public void accept(Student student) {
                    System.out.println(student);
                }
            });

            transaction.commit();
        }catch (Exception e){
            transaction.rollback();
            e.printStackTrace();
            throw new RuntimeException("出异常啦");
        }finally {
            entityManager.close();
        }
    }
}

通过多方查一方

            Student student = entityManager.find(Student.class, 5);
//            Student student = entityManager.getReference(Student.class, 5);
            School school = student.getSchool();
            System.out.println(school);

Demo源码地址

https://github.com/lunxinfeng/jpa