JPA快速入门(二)

大纲:

  1. 对象关系映射

    1. 关联关系映射
      1. 单向多对一
      2. 单向一对多
      3. 双向多对一
      4. 双向多对多
    2. 集合映射
    3. 组件关系映射
    4. 继承关系映射
  2. 数据查询

    1. Query查询
    2. NamedQuery查询
    3. NativeQuery查询
  3. 事务并发访问

    1. 隔离级别
    2. 悲观锁
    3. 乐观锁
  4. 一级缓存

  5. 二级缓存


对象关系

  1. 依赖关系
    如果A对象离开了B对象,A对象就不能正常编译,则A对象依赖B对象.
    在A类使用到了B(调用了B的方法,调用了B的字段).
  2. 关联关系
    A对象依赖B对象,并且把B对象作为A对象的一个字段,则A和B是关联关系.

按照多重性分:
1).一对一:一个A对象属于一个B对象,一个B对象属于一个A对象.
比如:QQ号码对应一个QQ空间.
2).一对多:一个A对象包含多个B对象.
比如:一个部门包含多个员工对象,此时我们使用集合来封装B对象..
3).多对一:多个A对象同属于一个B对象,并且每个A对象只能属于一个B对象.
比如:多个员工对象属于同一个部门.
设计表的时候:外键在many这一方/在开发设计中:添加一个many方对象的时候,往往通过下拉列表去选择one方.
4).多对多:一个A对象属于多个B对象,一个B对象属于多个A对象.
比如:一个老师可以有多个学生,一个学生可以有多个老师.
通过中间表来表示关系.
按照导航性分:如果通过A对象中的某一个属性可以访问该属性对应的B对象,则说A可以导航到B.
1).单向:只能从A通过属性导航到B,B不能导航到A.
2).双向:A可以通过属性导航到B,B也可以通过属性导航到A.
判断方法:
1,判断都是从对象的实例上面来看的;
2,判断关系必须确定一对属性;
3,判断关系必须确定具体需求;

  1. 聚合关系
    表示整体和部分的关系,整体和部分之间可以相互独立存在,一定是有两个模块来分别管理整体和部分.整体和部分可以分开.

  2. 组合关系
    强聚合关系,但是整体和部分不能独立存在,一定是在一个模块中同时管理整体和部分,生命周期必须相同.如:单据和单据明细/购物车信息

  3. 泛化关系
    其实就是继承关系.

在实际开发中,我们需要去维护对象之间的关系,意思是说,在保存A的时候,需要考虑到关联的B,在查询到A之后,如何将关联的B对象查询到

常用关联关系映射

  • 单向多对一
    在实际开发中,绝大部分的关联关系都是单向多对一,所以我们需要重点掌握

以员工和部门的关系为例

  1. 表结构的设计
    通常,多对一的关系,可以在多方的表中添加一个外键列来维护双方的关系,如:
    image.png

  2. 对象的设计

    @Getter@Setter
    public class Employee {
     private Long id;
     private String name;
     //封装当前员工所在的部门信息
     private Department dept;
    }
    
    @Getter@Setter
    public class Department {
     private Long id;
     private String name;
    }
    

    可以看到,在表结构方面,我们应该是在employee表中添加外键列来维护员工和部门的关系
    在对象设计方面,在Employee对象中关联Department对象,来封装当前员工所在的部门信息

  3. SQL分析
    保存(两个员工一个部门)
    因为是在多方(employee)关联one方(department)的数据,所以,在保存employee的时候需要考虑关系数据的维护(外键列的信息)

先保存部门,再保存员工

保存部门信息,将数据库自动生成的主键信息作为保存员工时的dept_id的信息
INSERT INTO department(name) VALUES(?);
这里的dept_id是刚刚保存的部门信息的id
INSERT INTO employee(name,dept_id) VALUES(?,?);
INSERT INTO employee(name,dept_id) VALUES(?,?);

先保存员工,再保存部门

此时dept_id为null
Employee emp = new Employee();
emp.setName(“Neld”);
emp.setDept(null);
INSERT INTO employee(name,dept_id) VALUES(?,null);
INSERT INTO employee(name,dept_id) VALUES(?,null);

INSERT INTO department(name) VALUES(?,?);

这三条sql执行之后,员工表的外键列为null,此时,需要发送两条update语句将dept_id更新进去
UPDATE employee SET dept_id = ?,name=? WHERE id = ?
UPDATE employee SET dept_id = ?,name=? WHERE id = ?

此时需要多发送两条sql才能将数据保存完整,所以,这种情况,建议选择先保存one方,再保存many方,尽量减少sql的发送

查询

查询id为1的员工,并获取到当前员工所在的部门信息
SELECT id,name,dept_id FROM employee WHERE id = ?;
查询部门是根据当前员工所在的部门的编号查询
SELECT id,name FROM department WHERE id = ?;

实现

使用JPA实现many2one的映射关系非常的简单,只需要在关联的对象上贴上@Many2One的注解即可,如:

@Getter
@Setter
@Entity
public class Employee {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne
    private Department dept;
}

测试代码

public void testSave() throws Exception {

    Employee e1 = new Employee();
    e1.setName("Neld");

    Employee e2 = new Employee();
    e2.setName("Lucy");

    Department dept = new Department();
    dept.setName("研发部");

    //设置关联关系
    e1.setDept(dept);
    e2.setDept(dept);

    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();
    //先保存one方,再保存many方
    em.persist(dept);
    em.persist(e1);
    em.persist(e2);

    em.getTransaction().commit();
    em.close();
}

执行的SQL:

Hibernate: insert into Department (name) values (?)

Hibernate: insert into Employee (dept_id, name) values (?, ?)

Hibernate: insert into Employee (dept_id, name) values (?, ?)

和预期的sql一致

如果先保存many方,再保存one方呢? 如:

em.persist(e1);
em.persist(e2);
em.persist(dept);

执行的SQL:

Hibernate: insert into Employee (dept_id, name) values (?, ?)
Hibernate: insert into Employee (dept_id, name) values (?, ?)
Hibernate: insert into Department (name) values (?)
Hibernate: update Employee set dept_id=?, name=? where id=?
Hibernate: update Employee set dept_id=?, name=? where id=?

可以看出,多执行了两条update语句来更新外键列的信息,而且更新语句中更新了除dept_id外键列以外的其他列的信息,说明这两条update语句必定是many方发出的,因为在one方是不知道有这些信息的

分析update语句的执行原理:

  1. 执行insert语句将Employee保存到数据库中,此时Employee对象由瞬时状态转换成持久状态,此时Employee所依赖的Department的id为null
  2. 保存Employee对象所依赖的Department对象,获取到数据库中自动生成的主键,此时一级缓存中的Employee对象发生改变(依赖的Department对象的id属性发生改变)
  3. 提交事务,检查缓存中的Employee对象和快照区域的Employee对象是否一致,发现两者所依赖的Department对象的主键信息是不一致的
  4. 一级缓存和快照区数据不一致,在提交事务的时候会执行对应的update语句将缓存中的脏数据持久化到数据库中

数据保存的细节分析:
现在创建表结构的sql是自动生成的,我们来做一个简单的分析

Hibernate: create table Employee (id bigint not null auto_increment, name varchar(255), dept_id bigint, primary key (id))

从建表语句中可以看出,为我们自动生成了关联Department的外键信息(dept_id)
外键列是有属性名_id组成的,如果需要修改,可以使用@JoinColumn注解修改外键列的信息

@ManyToOne
//修改外键列的相关信息(列名/约束/类型等)
@JoinColumn(name = "department_id")
private Department dept;

查询分析

测试代码

public void testGet() throws  Exception{

    EntityManager em = JPAUtil.getEntityManager();
    Employee e = em.find(Employee.class, 1L);

    System.out.println(e.getName());

    System.out.println(e.getDept().getName());

    em.close();
}

执行结果:

Neld

研发部

查询到了many方和many方关联的one的数据

SQL分析:

Hibernate: select employee0.id as id1_1_1, employee0.department_id as departme3_1_1, employee0.name as name2_1_1, department1.id as id1_0_0, department1.name as name2_0_0 from Employee employee0 left outer join Department department1 on employee0.department_id=department1.id where employee0_.id=?

可以看到,JPA默认使用的连接查询的方式,将many方和one的数据一起查询出来

当我们获取到主对象的同时,也需要获取到依赖的从对象的时候,这种方式还是不错的,

但是,在很多时候,我们不是每次都需要查询依赖的从对象信息,此时,我们就不需要每次在查询的时候,将这些数据查询出来了

那么,我们要如何实现,在我们需要的时候再发送sql查询关联的数据呢?

这就需要用到延迟加载了(也称为懒加载)

延迟加载

可以使用@Many2One注解中的fetch属性来解决,fetch属性的值定义在FetchType枚举类中,该枚举类中有如下两个值:

FetchType.EAGER:表示积极加载,缺省值

FetchType.LAZY:懒加载,按需加载,当访问依赖对象的时候,再发送sql去查询

在实际开发中,应根据需求合理选择延迟加载的使用

单向一对多

表结构的设计

表结构设计通常和单向多对一是一致的,也可以在many方添加外键列来维护两者之间的关系

但是,JPA是使用的中间表来维护关系,这样在性能方面来说,没有外键列这种设计方式好
image.png

对象的设计

@Entity
public class Employee {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
}
@Getter@Setter
@Entity
public class Department {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
//使用集合保存当前部门中所有的员工信息
    private List<Employee> employees;
}

SQL分析

  • 保存相关的sql
    INSERT INTO employee(name) VALUES(?)
    INSERT INTO employee(name) VALUES(?)

INSERT INTO department(name) VALUES(?)

需要执行两条保存关系数据到中间表的sql
INSERT INTO employee_department(employee_id,department_id) VALUES(?, ?)
INSERT INTO employee_department(employee_id,department_id) VALUES(?, ?)

  • 查询相关的sql
    查询出id为1的Department信息
    SELECT id,name FROM department WHERE id = ?

根据部门编号查询出当前部门所有的员工信息
SELECT e.id, e.name
FROM employee_department ed JOIN employee e on ed.employee_id = e.id
WHERE ed.department_id = ?

如果按照这种方式执行sql,那么数据是能够顺利的查询出来的

和单向多对一的问题是一样的,我们需要关心的是是否需要延迟加载及其控制的问题
实现

@Entity
public class Department {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @OneToMany
    private List<Employee> employees;
}

在one方依赖many方属性上贴上@One2Many注解即可,实现依然及其简单

保存的测试代码

@Test
public void testSave() throws Exception {

    Employee e1 = new Employee();
    e1.setName("Neld");

    Employee e2 = new Employee();
    e2.setName("Lucy");

    Department dept = new Department();
    dept.setName("研发部");

    List<Employee> employees = new ArrayList<>();
    employees.add(e1);
    employees.add(e2);
    //设置关联关系
    dept.setEmployees(employees);

    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();

    em.persist(e1);
    em.persist(e2);
    em.persist(dept);

    em.getTransaction().commit();
    em.close();
}

执行的SQL

Hibernate: insert into Employee (name) values (?)

Hibernate: insert into Employee (name) values (?)

Hibernate: insert into Department (name) values (?)

Hibernate: insert into Department_Employee (Department_id, employees_id) values (?, ?)

Hibernate: insert into Department_Employee (Department_id, employees_id) values (?, ?)

查询的测试代码

public void testGet() throws  Exception{
    EntityManager em = JPAUtil.getEntityManager();
    Department department = em.find(Department.class, 1L);
    System.out.println(department);
    System.out.println(department.getEmployees());
    em.close();
}

执行的SQL

Hibernate: select department0.id as id1_0_0, department0.name as name2_0_0 from Department department0 where department0.id=?

Department{id=1, name=’研发部’}

Hibernate: select employees0.Department_id as Departme1_0_1, employees0.employees_id as employee2_1_1, employee1.id as id1_2_0, employee1.name as name2_2_0 from DepartmentEmployee employees0 inner join Employee employee1 on employees0.employeesid=employee1.id where employees0_.Department_id=?

[Employee(id=1, name=Neld), Employee(id=2, name=Lucy)]

SQL分析:

首先,执行查询部门的sql,获取到对应的部门信息

然后,在访问当前部门对应的员工信息的时候,执行查询员工信息的sql

所以,在一对多的映射中,查询多方的数据,默认是使用延迟加载的方式查询

可以在@One2Many的注解中使用fetch属性指定加载的方式,可以使用FetchType.EAGE设置为积极加载

集合映射

首先观察下面代码中获取到的集合的类型

System.out.println(department.getEmployees().getClass());

输出结果: class org.hibernate.collection.internal.PersistentBag

可以看到,获取到的集合类型是被hibernate经过封装之后的PersitentBag,

下面就来介绍一下Hibernate封装的三个集合类:

  • PersistentList(List):

对应普通的List集合(元素有序/允许重复)

如果需要排序,在集合属性上贴@OrderColumn(name=”seq”)

JPA会在中间表中添加一个seq列用来记录顺序

image.png

  • PersistentSet(Set):

对应普通的Set集合(元素无序/不允许重复)

如果需要排序,在集合属性上贴@OrderBy(”name DESC”)注解

此时,JPA会根据name字段的值做降序排序

Hibernate: select employees0.department_id as departme1_0_1, employees0.employees_id as employee2_2_1, employee1.id as id1_1_0, employee1.name as name2_1_0 from employeedepartment employees0 inner join Employee employee1 on employees0.employeesid=employee1.id where employees0.department_id=? order by employee1.name desc

  • PersistentBag(List):

经过封装的List集合(元素有序/不允许重复)

如果使用List集合,JPA默认使用该类型

排序的方式和Set的方式一致

双向多对一

双向多对一,从导航性分析,既能从many方找到one方,也能从one方找到many方,所以对象的设计应该为

@Entity
public class Department {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @OneToMany
    private List<Employee> employees;
}
@Entity
public class Employee {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToOne
    private Department department;
}

在表结构上,问题在于,我们是使用外键列来维护关系还是使用中间表呢?当然是选择中间表了

但是,使用@One2Many的时候,JPA会为我们创建中间表,使用@Many2One的时候,JPA会为我们在many方的表中创建外键列,意思是,按照上面的实现方式,我们既有外键列又有中间表,这很明显是没有必要的

那我们能不能在集合属性上不加@One2Many注解,这样不就不会创建中间表了吗?

不好意思,这样是不可取的,因为这样的话,在查询的时候,JPA就不会帮我们去查询one方依赖的many的数据了

那该如何是好呢?

使用@One2Many中的mappedBy属性

该属性的含义是:

  1. 让one方放弃对关系的维护,意思是,在保存one方数据时,不用去维护和many方的关系,此时JPA就不会为我们创建中间表了

  2. 在查询one方依赖的many方数据时,直接根据这里配置的many方属性对应的数据进行查询

这样,双向的多对一在最终sql的执行上,和单向的多对一基本一致,最终达到关系的双向关联

@OneToMany(mappedBy = "department")
private List<Employee> employees;

单向多对多

表结构方面,仍然使用中间表维护双方的关系,和一对多的表结构一致

对象设计方面也和一对多一致,所以就不再赘述

最后在集合属性上贴上@Many2Many的注解即可

如果是双向的,需要让其中的一方放弃关系的维护,不然会有多余的sql发送出来

@Entity
public class Teacher {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToMany
    private List<Student> students;
}
@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToMany(mappedBy = "students")
    private List<Teacher> teachers;
}

测试代码:

public void testSave() throws  Exception{
    Teacher t1 = new Teacher();
    t1.setName("t1");
    Teacher t2 = new Teacher();
    t2.setName("t2");

    Student s1 = new Student();
    s1.setName("s1");
    Student s2 = new Student();
    s2.setName("s2");

    List<Student> students = new ArrayList<>();
    students.add(s1);
    students.add(s2);
    t1.setStudents(students);
    t2.setStudents(students);

    List<Teacher> teachers = new ArrayList<>();
    teachers.add(t1);
    teachers.add(t2);
    s1.setTeachers(teachers);
    s2.setTeachers(teachers);


    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();

    em.persist(t1);
    em.persist(t2);
    em.persist(s1);
    em.persist(s2);

    em.getTransaction().commit();
    em.close();
}

组件关系

公司(注册地址/营业地址)

需求:一个公司有两个地址,一个注册地址,一个营业地址,实现公司和对应地址信息的映射

实现思路:公司信息以及对应的地址信息保存在一张表中,那么最开始的时候我们会想到下面的对象设计方式:

@Entity
public class Company {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    //公司地址信息
    private String province;
    private String city;
    private String street;
    //注册地址信息
    private String reg_province;
    private String reg_city;
    private String reg_street;
}

将所有的地址信息封装到公司对象中

首先,这种方式是可以实现对公司的地址信息管理的

但是从对象封装上面来说,这种方式不够好,地址相关的字段在公司信息中重复了

所以,我们考虑将地址信息封装到一个单独的对象中

public class Address {
    private String province;
    private String city;
    private String street;
}

然后,公司对象封装就修改为:

@Entity
public class Company {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    //公司地址
    private Address address;
    //注册地址
    private Address regAddress;
}

所以,组件关系的对象设计就应该如此,那么如何映射呢?

在Address类上贴上@Embeddable注解,表示当前对象是可植入的,作为其他对象的组件存在

@Embeddable
public class Address {
    private String province;
    private String city;
    private String street;
}

使用该注解之后,存在一个问题,JPA将会在Company表中创建对应的列,但是Company中有两个Address对象,所以将会存在相同的列

此时,需要对其中一个地址对象的属性的列做映射,解决列名冲突的问题

private Address address;
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name= "reg_city")),
@AttributeOverride(name = "province", column=@Column(name="reg_province")),
@AttributeOverride(name = "street", column = @Column(name="reg_street"))
})
private Address regAddress;

继承关系

案例: 商品(图书/衣服)
需求: 商品(Product), 图书(Book), 衣服(Cloth),实现三者之间的继承关系维护

image.png

JPA可以使用三种方式来完成关系的维护:
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
每个实体类一张表,如:
image.png

这种方式的问题是
多态查询效率低下

@Inheritance(strategy = InheritanceType.JOINED)
每个子类一张表
image.png

这种方式的问题是
新增查询需要操作多张表,效率低下

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
image.png

这种方式的问题是
1.无法对具体的列做非空约束
2.需要添加额外的列来区分子类型

实际开发中,方式一使用较多,因为在查询性能方面,效率高
级联映射
什么是级联映射?
简单说,就是在保存瞬时状态的主对象的时候,同时将关联的处于瞬时状态的从对象一起持久化到数据库中,删除更新也是如此.

案例: 实现订单和订单明细的增删改

分析:
订单和订单明细属于组合关系,这种关系往往需要用到级联映射,因为他们是在一个模块中进行管理的

接下来,我们就来分析,如何级联管理这里的订单和订单明细的关系
casecade = CasecadeType.PERSIST
保存主对象的时候,同时将关联的对象持久化到数据库中
casecade = CasecadeType.REFRESH
查询主对象的同时,重新查询关联的对象
casecade = CasecadeType.REMOVE
删除主对象的同时,删除关联的对象
casecade = CasecadeType.DETACH
在主对象变为游离对象的同时,将关联的对象也转换成游离对象
casecade = CasecadeType.MERGE
在更新主对象的同时,更新关联的对象
casecade = CasecadeType.ALL
包含以上所有的级联关系
在开发中,我们应该根据需求选择合适的级联方式来管理主对象和关联对象

测试代码

@Getter@Setter@Entity
public class OrderBill {
    @Id
    @GeneratedValue
    private Long id;
    private String sn;

    //cascade:选择合适的级联方式,如果都需要,可以使用ALL
    //orphanRemoval:true,表示级联删除孤儿数据(当前不被关联的数据)
    @OneToMany(cascade = {CascadeType.PERSIST,CascadeType.REMOVE,
                CascadeType.MERGE},orphanRemoval = true)
    private List<OrderBillItem> items;
}

保存订单和明细

public void testSave() {
    OrderBill bill = new OrderBill();
    bill.setSn("sn");

    OrderBillItem item1 = new OrderBillItem();
    item1.setProductName("product1");
    OrderBillItem item2 = new OrderBillItem();
    item2.setProductName("product2");
    OrderBillItem item3 = new OrderBillItem();
    item3.setProductName("product3");

    List<OrderBillItem> items = new ArrayList<>();
    items.add(item1);
    items.add(item2);
    items.add(item3);
    bill.setItems(items);

    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();
    em.persist(bill);
    em.getTransaction().commit();
    em.close();
}

删除订单和明细

public void testDelete() {
    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();
    OrderBill bill = em.find(OrderBill.class, 1L);
    em.remove(bill);
    em.getTransaction().commit();
    em.close();
}

编辑订单和明细

public void testUpdate() {
    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();
    OrderBill bill = em.find(OrderBill.class, 1L);

    List<OrderBillItem> items = bill.getItems();
    //删除明细中的第一个元素
    items.remove(0);

    //创建一个新的明细信息添加到明细集合中
    OrderBillItem item = new OrderBillItem();
    item.setProductName("newitem");
    items.add(item);

    //获取到第一个明细并修改明细信息
    OrderBillItem oldItem = items.get(0);
    oldItem.setProductName("updateitem");

    //执行订单的更新,同时将上面对明细的修改持久化到数据库中
    em.merge(bill);

    em.getTransaction().commit();
    em.close();
}

数据查询

Query查询API
EM提供的创建Query方法:

  1. createQuery(String jpqlString):创建执行JPQL语句的查询对象
  2. createNamedQuery(String name):创建命名查询对象,执行事编写好的JPQL语句
  3. createNativeQuery(String sqlString):创建执行SQL语句的查询对象,直接执行传递进来的SQL语句
  4. createNativeQuery(String sqlString, Class resultClass):执行指定的SQL,将查询结果封装成指定的类型
  5. createNativeQuery(String sqlString, String resultSetMapping):执行指定的SQL,按照配置的resultSetMapping进行数据的封装

参数设置

List getResultList():执行查询多条数据
Object getSingleResult(); //如果结果只有一条,那么可以直接使用该方法执行查询
int executeUpdate(); //该句用于update,delete语句

分页查询相关

Query setFirstResult(int startPosition)
指定从哪一条开始查询
Query setMaxResult(int maxResult)
指定查询最多查询到多少条

使用JPQL查询步骤

(1)EntityManager创建Query对象
(2)如果包含参数,setParameter()
(3)如果需要分页,调用Query的setFirstResult()或者setMaxResult()
(4)如果是select语句,使用getResultList()或者getSingleResult();

使用Query和NamedQuery完成练习练习:

1.查询出所有的员工信息
2.查询出年龄大于20的员工信息
3.查询出年龄大于20的员工的姓名,年龄,雇佣日期
4.从第3个员工开始,查询5个员工信息
5.查询出所有的员工信息,按照年龄从大到小排序

JPQL语法分析

编写出下面SQL对应的JPQL
select * from user
user:表名
jpql:
select u from User u
User:类名,并为当前类起别名,使用该别名访问对象中的属性

使用JPQL可以完成SQL中的相关语法实现,如:

  • 排序:order by columnName [DESC/ASC]
  • 分组:group by…having…
  • 连接查询:left out join/left out join fetch/inner join 等
    left out join fetch: 和left out join 区别是,left out join fetch使用一条SQL将主从对象的数据同时查询出来封装到主对象中
  • 子查询

    事务并发访问

    事务概述
    事务并发访问和事务隔离级别:
    事务并发5类问题(如果数据库没有做任何并发处理的情况下):
  1. 第一类丢失更新:(撤销丢失)两个事务更新相同数据,如果一个事务提交,另一个事务回滚,第一个事务的更新会被回滚.

  2. 脏读:第二个事务查询到第一个事务未提交的更新数据,第二个事务根据该数据执行,但第一个事务回滚,第二个事务操作脏数据

  3. 虚读(幻读):一个事务查询到了另一个事务已经提交的新数据,导致多次查询数据不一致,认可第二次读取到的数据即可.

  4. 不可重复读:一个事务查询到另一个事务已经修改的数据,导致多次查询数据不一致,认可.

  5. 第二类丢失更新:(覆盖丢失)多个事务同时读取相同数据,并完成各自的事务提交,导致最后一个事务提交会覆盖前面所有事务对数据的改变.


一般情况,数据库都会处理一些事务并发的问题,数据库提供了不同的事务隔离级别来处理不同的事务并发问题,事务隔离级别定义如下:

  1. READ_UNCOMMITED:允许你读取还未提交的改变了的数据。可能导致脏、幻、不可重复读(相当于没有做任何事务隔离)

  2. READ_COMMITTED:允许在并发事务已经提交后读取。可防止脏读,但幻读和 不可重复读仍可发生(ORACLE默认级别)

  3. REPEATABLE_READ:对相同字段的多次读取是一致的,除非数据被事务本身改变。可防止脏、不可重复读,但幻读仍可能发生。(MYSQL默认级别)

  4. SERIALIZABLE:完全服从ACID的隔离级别,确保不发生脏、幻、不可重复读。这在所有的隔离级别中是最慢的,它是典型的通过完全锁定在事务中涉及的数据表来完成的。(ORACLE支持)
    数据库的隔离级别除了SERIALIZABLE,都不能处理第一类丢失更新和第二类丢失更新;
    所以,数据库提供了锁机制来防止第一类丢失更新和第二类丢失更新;

    悲观锁

    它指的是对数据被外界修改持保守态度。假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,为了保持数据被操作的一致性,于是对数据采取了数据库层次的锁定状态,依靠数据库提供的锁机制来实现。
    基于jdbc实现的数据库加锁如下:
    select * from account where name=”Erica” for update

在更新的过程中,数据库处于加锁状态,任何其他的针对本条数据的操作都将被延迟。本次事务提交后解锁。

但是,从系统的性能上来考虑,对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多联机,假设有数以百计或上千甚至更多的并发访 问出现,我们该怎么办?如果等到数据库解锁我们再进行下面的操作,我们浪费的资源是多少?这也就导致了乐观锁的产生。

乐观锁

乐观锁(optimistic locking)则乐观的认为资料的存取很少发生同时存取的问题,因而不作数据库层次上的锁定,为了维护正确的数据,乐观锁定采用应用程序上的逻辑实现版本控制的方法。
例如若有两个客户端,A客户先读取了账户余额100元,之后B客户也读取了账户余额100元的数据,A客户提取了50元,对数据库作了变更,此时数 据库中的余额为50元,B客户也要提取30元,根据其所取得的资料,100-30将为70余额,若此时再对数据库进行变更,最后的余额就会不正确。
在不实行悲观锁定策略的情况下,数据不一致的情况一但发生,有几个解决的方法,一种是先更新为主,一种是后更新的为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁定。

Hibernate 中通过版本号检查来实现后更新为主,这也是Hibernate所推荐的方式,在数据库中加入一个VERSON列记录,在读取数据时连同版本号一同读取,并在更新数据时递增版本号
以刚才的例子,A客户读取账户余额1000元,并连带读取版本号为5的话,B客户此时也读取账号余额1000元,版本号也为5,A客户在领款后账户 余额为500,此时将版本号加1,版本号目前为6,而数据库中版本号为5,所以予以更新,更新数据库后,数据库此时余额为500,版本号为6,B客户领款 后要变更数据库,其版本号为5,但是数据库的版本号为6,此时不予更新,B客户数据重新读取数据库中新的数据并重新进行业务流程才变更数据库。
以Hibernate实现版本号控制锁定的话,我们的对象中增加一个version属性,例如:

public class Account {
  private int version;
  public void setVersion(int version) {
    this.version = version;
  }
  public int getVersion() {
    return version;
  }
}

设定好版本控制之后,在上例中如果B 客户试图更新数据,将会引发StableObjectStateException 异常,我们可以捕捉这个异常,在处理中重新读取数据库中的数据,同时将 B客户目前的数据与数据库中的数据读出来,让B客户有机会比对不一致的数据,以决定要变更的部份,或者您可以设计程式自动读取新的资料,并重复扣款业务流程,直到数据可以更新为止,这一切可以在背后执行,而不用让您的客户知道。

但是乐观锁也有不能解决的问题存在:上面已经提到过乐观锁机制的实现往往基于系统中的数据存储逻辑,在我们的系统中实现,来自外部系统的用户余额更新不受我们系统的控制,有可能造成非法数据被更新至数据库。因此我们在做电子商务的时候,一定要小心的注意这项存在的问题,采用比较合理的逻辑验证,避免数据执行错误。

悲观锁与乐观锁的比较:

悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受;

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

JPA 2.0 锁机制

锁是处理数据库事务并发的一种技术,当两个或更多数据库事务并发地访问相同数据时,锁可以保证同一时间只有一个事务可以修改数据。

锁的方法通常有两种:乐观锁和悲观锁。乐观锁认为多个并发事务之间很少出现冲突,也就是说不会经常出现同一时间读取或修改相同数据,在乐观锁中,其目标是让并发事务自由地同时得到处理,而不是发现或预防冲突。两个事务在同一时刻可以访问相同的数据,但为了预防冲突,需要对数据执行一次检查,检查自上次读取数据以来发生的任何变化。
悲观锁认为事务会经常发生冲突,在悲观锁中,读取数据的事务会锁定数据,在前面的事务提交之前,其它事务都不能修改数据。

JPA 2.0增加了6种新的锁模式,其中两个是乐观锁。JPA 2.0也允许悲观锁,并增加了3种悲观锁,第6种锁模式是无锁。

下面是新增的两个乐观锁模式:

1、OPTIMISTIC:它和READ锁模式相同,JPA 2.0仍然支持READ锁模式,但明确指出在新应用程序中推荐使用OPTIMISTIC。

2、OPTIMISTIC_FORCE_INCREMENT:它和WRITE锁模式相同,JPA 2.0仍然支持WRITE锁模式,但明确指出在新应用程序中推荐使用OPTIMISTIC_FORCE_INCREMENT。

下面是新增的三个悲观锁模式:

1、PESSIMISTIC_READ:只要事务读实体,实体管理器就锁定实体,直到事务完成锁才会解开,当你想使用重复读语义查询数据时使用这种锁模式,换句话说就是,当你想确保数据在连续读期间不被修改,这种锁模式不会阻碍其它事务读取数据。

2、PESSIMISTIC_WRITE:select …for update只要事务更新实体,实体管理器就会锁定实体,这种锁模式强制尝试修改实体数据的事务串行化,当多个并发更新事务出现更新失败几率较高时使用这种锁模式。

3、PESSIMISTIC_FORCE_INCREMENT:当事务读实体时,实体管理器就锁定实体,当事务结束时会增加实体的版本属性,即使实体没有修改。
你也可以指定新的锁模式NONE,在这种情况下表示没有锁发生。

JPA 2.0也提供了多种方法为实体指定锁模式,你可以使用EntityManager的lock() 和 find()方法指定锁模式。此外,EntityManager.refresh()方法可以恢复实体实例的状态。

一级缓存

EntityManager级别的缓存机制

二级缓存

EntityManageFactory级别的缓存机制

所谓的二级缓存,也就是可以跨entityManager的缓存,也就是说:就算你关闭了entityManager,缓存也依然在。
在配置文件persistence.xml中配置
<!-- 二级缓存相关 -->

<property name="hibernate.cache.use_second_level_cache" value="true"/>

<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>

<property name="hibernate.cache.use_query_cache" value="true"/>
缓存需要以下jar包:
image.png

在src下加入一个配置文件:ehcache.xml,
共同的配置:
1,maxElementsInMemory:该缓存池放在内存中最大的缓存对象个数;
2,eternal:是否永久有效,如果设置为true,内存中对象永不过期;
3,timeToIdleSeconds:缓存对象最大空闲时间,单位:秒;
4,timeToLiveSeconds:缓存对象最大生存时间,单位:秒;
5,overflowToDisk:当内存中对象超过最大值,是否临时保存到磁盘;
6,maxElementsOnDisk:能保存到磁盘上最大对象数量;
7,diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒
8,memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。
默认策略是LRU(最近最少使用),可以设置为FIFO(先进先出)或是LFU(较少使用)

<ehcache>
    <diskStore path="java.io.tmpdir"/>
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />

    <cache name="sampleCache1"
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="true"
        />
</ehcache>

启用二级缓存:
1.在实体类上加注解@Cacheable(true)
@Cacheable(true)
@Table(name=”T_USER”)
@Entity
public class User …
2.在配置文件persistence.xml中配置二级缓存的策略
<!-- 配置二级缓存的策略 ALL:所有的实体类都被缓存 NONE:所有的实体类都不被缓存. ENABLE_SELECTIVE:标识 @Cacheable(true) 注解的实体类将被缓存 DISABLE_SELECTIVE:缓存除标识 @Cacheable(false) 以外的所有实体类 UNSPECIFIED:默认值,JPA 产品默认值将被使用 -->

<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
注意:这个配置要放在provider 节点和class 节点后面
再次执行
User user1 = entityManager.find(User.class, 1);
entityManager.close();
entityManager = factory.createEntityManager();
User user2 = entityManager.find(User.class, 1);
结果只调用了一次sql查询语句,说明二级缓存 起作用了。

猜你喜欢

转载自blog.csdn.net/wolfcode_cn/article/details/81169902
今日推荐