文章目录
- jpa
- jpa(这东西针对Java,准确的说是JavaEE,而不是spring)与hibernate
- 开始学习
jpa
jpa(这东西针对Java,准确的说是JavaEE,而不是spring)与hibernate
所以这个不需要spring就可以使用?对
hibernate是一个开放的对象关系映射框架,它对jdbc进行了非常轻量级的封装,将pojo与数据库表建立映射关系,是一个全自动的orm框架,可以自动生成sql语句,自动执行。
jpa的全称是java persistence(持续、存留,即持久化) api,是sun公司推出的一套基于orm的规范(java ee5.0平台标准的orm规范,以统一的方式访问持久层,还真是针对的Java,汗),注意不是orm框架,因为jpa并未提供orm实现,它只提供了一些编程的api接口(接口就不是实现了)。
总结:
hibernate是jpa的实现,但是hibernate(从3.2版本开始兼容jpa)不仅仅遵循jpa的接口(规范),还遵循其他规范,意思大概是:jpa现在是hibernate的子集。
ps:orm:
简单说,ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了 。
ORM 把数据库映射成对象。
- 数据库的表(table) --> 类(class)
- 记录(record,行数据)–> 对象(object)
- 字段(field)–> 对象的属性(attribute)
开始学习
orm映射元数据
通过xml和jdk5.0注解将实体对象持久化到数据库中
jap的api
操作实体对象进行crud,而不是sql语句或者jdbc
jpql(喵喵喵?刚刚说不用sql,自己还整一个jpql)
面向对象的查询语言(为了面向对象而面向对象,哈哈)
架构
基本步骤
-
创建maven项目(空的maven项目)以及
persistence.xml
,在这个文件中进行配置-
要求的目录结构:在src/main/resource/META-INF中
-
指定数据库
-
使用哪个orm框架以及配置该框架的基本属性
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yyh</groupId> <artifactId>jpa_demo</artifactId> <version>1.0-SNAPSHOT</version> <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>5.2.16.Final</version> </dependency> <!--jpa的jar包--> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>5.2.16.Final</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.18</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> </project>
-
-
创建实体类,用注解(
@Entity
)来描述实体类跟表的映射关系 -
用jpa的api完成数据crud
- 创建
EntityManagerFactory
(对应hibernate的SessionFactory
,估计也是mybatis的SqlSessionFactory
) - 创建EntityManager(对应hibernate的
Session
,估计是mybatis的SqlSession
)
- 创建
注解
-
@Entity
实体类上的注解,默认使用当前类名作为
jpql
中的类名;如果这样
@Entity(name = "UserInfo")
,jpql就不能写成select u from User u
,而应该是select u from UserInfo u
。(数据库中的表名也会改掉) -
@Table
映射的表信息(如表名),默认表名和类名一致,如果类名与表名不一致需要添加此注解(表名的大小写不影响,但是属性名与列名大小写不一样会被认为不一致),如:
@Table(name = "t_user")
映射的表名为t_user
-
@Column(name = "hiredate")
如果属性名与列名不一致需要添加此注解
-
@GeneratedValue(strategy = GenerationType.AUTO)
指定主键的生成策略:
-
strategy = GenerationType.AUTO
默认方式生成主键,根据具体的数据库选择合适的策略,可能是
Table/Sequence/Identity
中的一种,如果是Oracle
,就会选择Sequence
,如果是mysql
,那就是自增长(自动建表是就会指定AUTO_INCREMENT
)。 -
strategy = GenerationType.IDENTITY
多数数据库支持,自增长,比如
mysql
在自动建表时就会指定AUTO_INCREMENT
(自增长),Oracle
不支持该策略 -
strategy = GenerationType.TABLE
单独创建一张表维护主键信息(一共两列,第一列是拥有的表名,第二列是对应表的主键最大值),在不同的数据库之间移植性好,如在
mysql
中就会额外生成hibernate_sequences
(默认生成的名字,可以改的,用@TableGenerator()
)表,但是不建议优先使用。 -
strategy = GenerationType.SEQUENCE
Oracle
虽然不支持自增长,但是支持该策略:序列。支持的数据库还有:PostgreSQL、DB2
-
-
@Temporal(TemporalType.DATE)
指定日期属性对应数据库的
date
类型而不是timestamp
。除了这两种类型之外还有datetime
(全指的是数据库中的日期类型) -
@Column
属性与列名映射,除了放在属性上还可以放在getter上
-
name
:列名。默认使用属性名作为列名,两者一致的时候就不需要指定name
了,注意大小写。 -
unique
:唯一约束 -
nullable
:非空约束 -
insertable:false
,表示生成insert语句时不插入这一列的值 -
updatable:false
,表示生成update语句时不更新这一列的值 -
length
:指定该列的长度 -
columnDefination
:自定义列的类型,jpa会默认根据属性的类型自动生成 -
precision
:在使用decimal类型的时候指定总长度 -
scale
:在使用decimal类型的时候指定小数位数
ps:在实际开发中,
@Access(AccessType.PROPERTY):getter方法上和@Access(AccessType.FIELD):属性上
可以告诉jpa只去扫描哪个位置上的@Column
注解,因为默认情况既会去属性上面找也会去getter方法上面去找。 -
-
@Transient
因为jpa默认对实体类所有属性进行映射,对不需要持久化的属性添加该注解,java序列化表示这个我熟。
-
@Lob
大数据类型的映射,该注解可以对应
text/blob/clob
类型映射,因为String
默认映射的类型时varchar
(最多255字节),现在需要大文本、文章,如:@Lob private String content;
-
@JoinColumn(name = "dept_id")
jpa可以自动建表生成外键列,默认名是
属性名_id
,可以使用该注解修改外键列字段的名称
EntityManagerFactory和EntityManager对象的创建
EntityManagerFactory用来创建EntityManager和mybatis很像,EntityManagerFactory是线程安全的,多个线程可以共用一个EntityManagerFactory,该对象比较消耗资源,通常一个项目只有一个该对象。
EntityManager非线程安全,所以每次数据库的访问,应该创建一个新的EntityManager对象
JPAUtil工具类
package com.yyh.util;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class JPAUtil {
private static EntityManagerFactory emf;//这个应用中只需要创建一个该对象,所以放在静态代码块里面
static {
emf = Persistence.createEntityManagerFactory("myPersistence");//参数是持久单元名字
}
private JPAUtil(){}//工具类构造器私有化,保证单例
public static EntityManager getEntityManager(){
return emf.createEntityManager();//创建EntityManager对象,即连接对象
}
}
crud
package com.yyh.jpa_01_crudtest;
import com.yyh.jpa_01_crud.User;
import com.yyh.util.JPAUtil;
import org.junit.Test;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.Date;
import java.util.List;
public class CRUDTest {
//保存
@Test
public void testSave(){
User user = new User();
user.setName("sansan");
user.setAge(10);
user.setHireDate(new Date());
//调用EntityManager完成保存
EntityManager em = JPAUtil.getEntityManager();
//开启事务
em.getTransaction().begin();
//执行保存
em.persist(user);
//提交事务
em.getTransaction().commit();
//释放资源
em.close();
}
//删除
@Test
public void testDelete(){
//调用EntityManager
EntityManager em = JPAUtil.getEntityManager();
//开启事务
em.getTransaction().begin();
//执行删除
//先查询到要删除的数据
User user = em.find(User.class, 1L);
em.remove(user);
//提交事务
em.getTransaction().commit();
//释放资源
em.close();
}
//更新
@Test
public void testUpdate(){
User user = new User();
user.setId(2L);
user.setName("pangpang");
user.setAge(12);
user.setHireDate(new Date());
//调用EntityManager
EntityManager em = JPAUtil.getEntityManager();
//开启事务
em.getTransaction().begin();
//执行更新
em.merge(user);
//提交事务
em.getTransaction().commit();
//释放资源
em.close();
}
//查询单条数据
@Test
public void testGet(){
//调用EntityManager
EntityManager em = JPAUtil.getEntityManager();
User user = em.find(User.class, 2L);//参数1:结果封装成什么类型的对象;参数2:指定查询的主键
System.out.println(user);
em.close();
}
//查询多条数据
@Test
public void testList(){
//调用EntityManager
EntityManager em = JPAUtil.getEntityManager();
//参数1:指定执行的jpql;参数2:查询的结果封装类型
TypedQuery<User> query = em.createQuery("select u from User u", User.class);
//结果放在list集合里面
List<User> list = query.getResultList();
System.out.println(list);
em.close();
}
}
hbm2ddl工具的使用
用途:让jpa自动生成表结构
<!--jpa自动生成表结构-->
<property name="hibernate.hbm2ddl.auto" value="create"/>
-
hibernate.hbm.auto
为create
会先删除实体对应的表,再创建
-
hibernate.hbm.auto
为create-drop
与
create
一致,只是在关闭系统前会删除jpa管理的所有表(EntityManagerFactory关闭之后才会执行删除) -
hibernate.hbm.auto
为update
启动时,检查实体类和表结构是否有变化,如果有,则更新表结构(可以感知添加属性,不能感知删除的属性,也不能感知修改的属性类型)
-
hibernate.hbm.auto
为validate
启动时,检查实体类和表结构是否有变化,如果有,启动失败,抛出异常
ps:开发阶段选择
create
或create-drop
;测试阶段选择update
;生产环境选择validate
,有助于发现表结构的问题。
persistence.xml
一些配置说明
-
<class>
:需要扫描的实体类 -
<exclude-unlisted-classes>
为true
时,表示不扫描上面没有列出来的类 -
<jar-file>
:对项目中引入的jar包中的类进行扫描,因为在实际开发中需要让jpa去扫描jar包引进的类,这些类有可能需要做数据库持久化。
一级缓存
在EntityManager
中存在一个缓存区域(一级缓存),jpa会将查询到的对象缓存到该区域中。如果在同一个EntityManager
中,查询相同的OID
(应该是Object ID的意思)的数据,只会发送一条sql,在事务提交或者EntityManager
关闭之后一级缓存会清空,不同的EntityManager
使用不同的一级缓存。
手动清除一级缓存:
- detach:清除一级缓存中指定的对象
- clear:清除一级缓存中使用的缓存数据
一级缓存能力比较有限
延迟加载
根据主键查询有两个方法:
find()
和getReference()
后者就是延迟加载,只会在使用到查询的对象的时候才会发送该查询的sql(不能在em.close();
使用,否则会报no session
的错误)。
原理:
动态代理重写了对象的getter方法,在getter方法中执行sql。
ps:find()
没有查到数据会返回null,而``getReference()不会返回null,会抛出异常
EntityNotFoundException`
对象的状态(这个没意义)
-
瞬时状态
使用
new
关键字创建出来的对象,没有OID。 -
持久状态
对象保存到数据库中后,对象状态转化为持久状态,在一级缓存,有OID
-
游离状态
对象存在于数据库中,但是不在一级缓存中,有OID
-
删除状态
事务一旦提交,对象就会从数据库中删除,是介于持久状态和被删除之间的一个临界状态。在一级缓存,有OID
管理对象状态的方法:
persist()
:保存,转换为持久状态
remove()
:删除
merge()
:更新,将游离实体转化为持久状态
ps:注意merge()
:当对象存在OID时,执行更新,反之执行保存
事务改变状态的方法:
commit()
和rollback()
有一个有意思的现象(看注释):
@Test
public void testObjectState(){
EntityManager em = JPAUtil.getEntityManager();
em.getTransaction().begin();
User user = em.find(User.class, 1L);
user.setName("pangpang");
em.getTransaction().commit();//此处会自动执行更新语句
em.close();
}
原因:数据从数据库查出来之后,在内存中会有两份数据,一份在一级缓存中,另一份在EntityManager
的快照区,两份数据一样,修改user的name时,修改的是缓存的数据,又因为在事务提交的时候会清空一级缓存,此时会比较两份数据是否一致,若不一致,就会发送update
的sql语句将缓存中的脏数据(和数据库中的数据不一致)同步到数据库中。
对象关系
-
依赖关系
如果A对象离开了B对象,A对象就不能正常编译,则A对象依赖B对象。如:
在A中使用到了B(调用了B的方法或属性)
-
关联关系
A对象依赖B对象,并且把B对象作为A对象的一个属性(成员变量),则A和B是关联关系
按照多重性分:
-
一对一:一个A对象属于一个B对象,反之也成立
-
一对多:一个A对象包含多个B对象
比如一个部门包含多个员工,用集合封装B对象
-
多对一:多个A对象属于一个B对象,且每个A对象只能属于一个B对象(这项没意义,因为例子跟上面一样)
-
多对多:一个A对象属于多个B对象,一个B对象属于多个A对象
比如一个老师对多个学生,一个学生对多个老师(中间表解决)
按导航性分:(如果通过A对象中的某一个属性可以访问该属性对应的B对象,即A可以导航到B)
- 单向:只能A导航到B,B不能导航到A
- 双向:A可以导航到B,B也可以导航到A
判断方法:
- 从对象出发
- 从属性出发
- 确定具体需求
-
-
聚合关系
表示整体和部分的关系,整体和部分之间可以相互独立存在。比如员工跟部门
-
组合关系
强聚合关系,也分为整体和部分,但是整体和部分不能独立存在。如,单据和单据明细(买的东西)
-
泛化关系
继承关系
单向多对一
以员工和部门为例,需求是员工到部门是单向多对一:
外键(dept_id)设置在员工表(many方)中,员工类中有一个部门属性,但是部门类中没有员工集合(因为单向)。在many方使用@ManyToOne
注解
-
保存
因为要维护外键所在的列,建议先有了部门(one方)再去保存员工(many方)信息,不然,可能无法保存员工信息或者要多发几条sql语句(jpa是后者)。
-
查询
jpa默认使用连接查询(左外连接,员工为主表)将many方和one方一起查询出来,但有时候并不需要查询one方,破解方法为延迟加载,六啊。使用
Many2One
注解中的fetch
属性,fetch
属性的值定义在FetchType
枚举类中,有两个值:FetchType.EAGER
,积极加载,默认值FetchType.LAZY
,按需加载,当访问依赖的对象(即one方)的时候再去发送sql查询。此时就不再是连接查询了,而是分开查询。
单向一对多
表结构与上方一致,外键(dept_id)仍然设置在员工表(many方)中,这种方法也可以实现,但是jpa不是这样。jpa是生成一张中间表(中间表存在外键来着,但员工表不再有外键的列),以此维护表关系,此时在部门类中就有员工集合了,但是员工类中没有部门属性。在one方使用@OneToMany
-
保存
只需要保存员工和部门就行了(此处就没有先后之分了,one方或者many方哪个先都一样,因为是中间表维护),中间表不需要手动维护。
-
查询
@Test public void testGet(){ EntityManager em = JPAUtil.getEntityManager(); Dept d = em.find(Dept.class, 1L); System.out.println(d); List<Employee> employeeList = d.getEmployeeList(); //默认使用延迟加载,在真正使用employeeList时才会去发送第二条连接查询的sql System.out.println(employeeList); em.close(); }
根据部门查询出当前部门的所有员工信息,jpa先单表查出部门,然后默认用中间表和员工表的内连接查询(join on)员工集合,默认使用延迟加载,在真正使用
employeeList
时才会去发送第二条连接查询的sql,这个也可以改成积极加载,方法同上。 -
有意思的现象:
在部门的员工集合属性不能使用
ArrayList
,因为hibernate实现了List
叫PersistentBag
。-
PersistentBag
有变化的java的
List
集合,元素有序,不允许重复,如果属性写的List
,默认使用PersistentBag
,如果需要排序,在集合的属性上加@OrderBy("name DESC")
,此时jpa根据name
的值降序排序,默认升序(对应sql的ASC
):@OrderBy("name")
-
PersistentSet
实现的java的
Set
集合,元素无序,不允许重复,排序方式同上 -
PersistentList
实现的java的
List
集合,元素有序,允许重复,如果需要排序,在集合的属性上加@OrderColumn(name = "lalala")
,此时jpa会在中间表添加lalala
列来记录排序。
-
双向多对一(用得非常少)
从导航性分析,既能从many方到one,也能从one方到many方。在部门类有员工集合,在员工类有部门属性。在one方(部门)使用@OneToMany
注解,在many方(员工)使用@ManyToOne
注解。默认情况即存在员工表的外键列,也会有中间表,所以,冗余了,推荐使用员工表的外键列来维护关系,所以在one方做一些改变@OneToMany(mappedBy = "dept")
,让one方放弃关系的维护,即不会创建中间表,注解括号中的值意思是指定many方中的dept
属性映射,以此维护。
-
保存
保存时与单向多对一一致,也有顺序问题
-
查询
测试方法与单向多对一一模一样,所以查询时的sql就一个,即连接查询(左外连接,员工为主表)。延迟加载同单向多对一。
单向多对多
以老师和学生为例(从老师可以导航到学生),使用中间表维护双方的关系,和单向一对多的表结构一致,类的设计方面也和单向一对多一致,最后在集合属性上加上@ManyToMany
的注释就行了,如果是双向的,需要让其中一方放弃关系的维护(比如在Student
类中的老师集合teacherList
属性加上@ManyToMany(mappedBy = "studentList")
,这样是让Student
类放弃关系的维护),不然会建两张中间表(teacher_student
和student_teacher
)。
组件关系
零件关系。
需求:一个公司有两个地址:注册地址、营业地址。实现公司和对应地址的映射。
思路:将地址单独建一个类,在公司类中关联俩地址(俩地址的类型相同),但是最终还是一张表(公司表),而不是两张表。
步骤:
-
在
Address
类上加@Embeddable
注解,表示此类生成的对象是作为其他对象的组件存在的 -
在
Company
类中@AttributeOverrides( { @AttributeOverride(name = "province",column = @Column(name = "reg_province")), @AttributeOverride(name = "city",column = @Column(name = "reg_city")), @AttributeOverride(name = "street",column = @Column(name = "reg_street")) } ) private Address regAddress;//公司注册地址
修改注册地址各个属性在表中的列名
继承关系
需求:商品(图书/衣服),图书和衣服继承自商品
jpa可以使用三种方式来完成关系的维护:
-
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
每个实体类就会是一张表,一共三张表,缺点:多态查询效率低下
//这个多态 Product book = em.find(Product.class, 2L); System.out.println(book);
这个查询的语句是有子查询:
SELECT product0_.id AS id1_2_0_, product0_.name AS name2_2_0_, product0_.ISBN AS ISBN1_0_0_, product0_.color AS color1_1_0_, product0_.clazz_ AS clazz_0_ FROM (SELECT id, NAME, NULL AS ISBN, NULL AS color, 0 AS clazz_ FROM Product UNION SELECT id, NAME, ISBN, NULL AS color, 1 AS clazz_ FROM Book UNION SELECT id, NAME, NULL AS ISBN, color, 2 AS clazz_ FROM Cloth) product0_ WHERE product0_.id = ? com.yyh.jpa._07_extends.Book @57 abad67
-
@Inheritance(strategy = InheritanceType.JOINED)
父表是一个通用的属性的表(但它不是商品表),含有三个类都有的属性
name
,然后每个子类一张表,子表的id来自于父表的id,子表有自己特殊的属性,缺点:新增或查询时需要操作多张表,效率低下。如果像上方一样进行多态查询,语句更复杂,效率比其他两种方式更低一些。
-
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
三种类的所有属性放到一张表中,缺点:无法对具体的列做非空约束;需要添加额外的列区分子类型
方式1和2查询效率稍低,因为有多个表,但是保存数据方面比较完整(可以添加非空约束),方式3没法保证数据完整性(非空约束),但查询效率高;方式1使用较多。
没有使用上方的注解会默认使用第三种方式(单表)。
当我使用方式1时:
Caused by: org.hibernate.MappingException: Cannot use identity column key generation with <union-subclass> mapping for: com.yyh.jpa._07_extends.Book
at org.hibernate.persister.entity.UnionSubclassEntityPersister.<init>(UnionSubclassEntityPersister.java:95)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
... 28 more
报错说的是identity
的生成策略不行,其实默认设置的是auto
,因为是mysql数据库,设置identity
和auto
最后都是自增长,即identity
,只能设置成table
,即单独建一张表来维护主键。
级联映射
在保存瞬时状态的主对象时,也将关联的处于瞬时状态的从对象一起持久化到数据库中,删除更新也是这样,意思是只需要持久化(crud)主对象,不需要持久化从对象(关联对象),从对象自动持久化。
需求:实现订单和订单明细的增删改
思路:订单和订单明细属于组合关系(整体和部分,分开不能独立存在),这种关系需要用到级联映射因为它俩是在一个模块中进行管理的(采用单向一对多)。
@OneToMany(casecade=CasecadeType.PERSIST)
-
casecade=CasecadeType.PERSIST
保存主对象的时候,同时将关联的对象持久化到数据库中。
-
casecade=CasecadeType.REFRESH
查询主对象的同时,重新查询关联的对象
-
casecade=CasecadeType.REMOVE
删除主对象的同时,删除关联的对象
-
casecade=CasecadeType.DETACH
在主对象变为游离对象的同时,将关联的对象也转换成游离对象。
-
casecade=CasecadeType.MERGR
在更新主对象的同时,更新关联的对象
//删除一个订单明细 orderItemList.remove(1);//在em.merge(orderBill);时,默认只会删除中间表里面的关系,不会删除订单明细里面的该记录 要删除的话,需要在@OneToMany加orphanRemoval = true,即删除孤儿
-
casecade=CasecadeType.ALL
包含以上所有的级联关系
ps:在jpa中,查询出来的数据会放在两个区域:一级缓存(业务过程中应该是修改的此处的数据)、快照区,在提交事务的的时候会检查两个区域的数据是否一致,一致则无事,不一致就会执行对应的更新的sql(如果是添加就执行添加)。
jpa提供的查询
em提供的Query方法
-
createQuery(String jpqlString)
-
createNamedQuery(String name)
事先将查询的jpql编写好,创建命名查询,指定执行对应的jpql。
需要在实体类上面jia
@NamedQueries
或@NamedQuery
-
createNativeQuery(String sqlStrig)
直接执行传递进来的sql语句
-
createNativeQuery(String sqlString,Class resultClass)
指定执行的sql,将查询结果封装成指定的类型
-
createNativeQuery(String sqlString,String resultSetMapping)
执行sql,然后解决列名跟属性不一样的问题。
参数设置(Query接口的方法)
-
List getResultList()
查询多条数据
-
Object getSingleResult()
查询单条数据,对应上方的方法,用于
select
语句 -
int executeUpdate()
用于
update,delete
语句 -
Query setFirstResult(int startPosition)和Query setMaxResult(int maxResult)
用于分页查询,前者是从哪条数据开始,后者是查多少条
使用jpql查询的步骤
- 用em创建
Query
对象 - 如果包含参数,使用
Query
的setParameter()
- 如果需要分页,调用
Query
的setFirstResult
或setFirstResult
- 如果是selec语句,使用
getResultList
或者getSingleResult
事务
事务并发访问五类问题(如果数据库没有做任何并发处理的情况下)
-
第一类丢失更新
两个事务更新相同数据,如果一个事务提交,另一个事务回滚,第一个事务的更新会被回滚。(第一个事务的更新结果被第二个事务的回滚覆盖掉了)
-
脏读
第二个事务查询到第一个事务未提交的更新数据,第二个事务根据该数据执行,但第一个事务回滚,第二个事务操作的是脏数据
-
虚读(幻读)
一个事务查询到了另一个事务已经提交的新数据,导致多次查询数据不一致,认可第二次读取到的数据即可
-
不可重复读
一个事务查询到另一个事务已经修改的数据,导致多次查询数据不一致(应该也是认可第二次读取到的数据)
-
第二类丢失更新
多个事务同时读取相同数据,并完成各自的事务提交,导致最后一个事务提交会覆盖前面所有事务对数据的改变。(需要重点解决)
事务隔离级别
一般情况数据库都提供了不同的事务隔离级别来处理不同的事务并发问题,如下:
-
READ_UNCOMMITED
允许读取还未提交的改变了的数据,可能导致脏、幻、不可重复读(相当于没有做任何事务隔离)
-
READ_COMMITED
允许并发事务已经提交后读取,可防止脏读,但幻读和不可重复读仍可发生(
ORACLE
默认级别) -
REPEATABLE_READ
对相同字段的多次读取是一致的,除非数据被事务本身改变。可防止脏、不可重复读,但幻读仍可能发生。(
mysql
默认级别) -
SERIALIZABLE
完全服从ACID的隔离级别,确保不发生脏、幻、不可重复读。这在所有的隔离级别中是最慢的,它是典型的通过完全锁定在事务中涉及的数据表来完成的。(
ORACLE
支持)
数据库的隔离级别除了SERIALIZABLE
,都不能处理第一类丢失更新和第二类丢失更新,所以数据库提供了锁机制来防止第一类丢失更新和第二类丢失更新。
悲观锁(像互斥的意思)
假定任何时刻存取数据时,都有可能有另一个客户也正在存取同一笔数据,为保持数据被操作的一致性,依靠数据库提供的锁进行锁定:
select * from account where name = "yyh" for update
数据库处于加锁状态,任何其他针对本条数据的操作都将被延迟,本次事务提交后解锁。
乐观锁
悲观锁存在的问题:从系统性能上考虑,对于单机或小系统而言,没什么问题,但如果是网络上的系统(暗示分布式?),同时间会有许多联机,若有成百上千或更多的并发访问出现,等到数据库解锁再进行下面的操作会浪费很多资源,所以,悲观锁no,乐观锁yes。
乐观锁认为很少发生同时存取的问题,因此不在数据库上锁定,为了维护正确的数据,采用应用程序上的逻辑实现版本控制的方法。
问题:假如有两个客户端,A先读取了账户余额100元,然后B也读取了账户余额100元,A提取了50元,对数据库做了变更,数据库中余额为50元,B要提取30元,根据B的资料:100-30,余额将会是70元,若此时对数据库进行变更,最后的余额就会不正确。
有几个解决方法:一种是先更新为主,一种是后更新的为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁定。
hibernate是通过版本号检查来实现后更新为主(hibernate推荐),在数据库中加入一个VERSION
列,在读取数据时,连同版本号一同读取,并在更新数据时递增版本号。
解决刚刚的例子:A读取账户余额100元,并读取版本号为5,B也读取余额100元,版本号也是5,A在领款后账户余额为50,此时将版本号加1变为6,而数据库中版本号为5,所以予以更新(提交事务),更新数据库后,数据库中的VERSION
变为6,而B稍后提交事务的update语句中 ,where条件中的版本号还是5,所以B会出现这样的结果:受影响的行数为0。(因为该条VERSION
为6,B的语句中还是5,即update时查不到记录)。所以,程序中就能够根据受影响的行数来判断是更新成功还是失败,失败时可以将结果告知用户,如:系统繁忙,稍后重试。
两种锁的比较
悲观锁大多数情况下可依靠数据库的锁机制实现,查询性能不高,但是增删改时,很安全。乐观锁效率高。但也有局限性:有可能造成非法数据被更新至数据库(有办法:不对外公开数据库表,将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径)。
jpa2.0的6中锁模式(乐观锁x2,悲观锁x3,无锁x1)
-
OPTIMISTIC
在表中会加上
VERSION
列。 -
OPTIMISTIC_FORCE_INCREMENT
-
PESSIMISTIC_READ
只要事务读实体,就锁定实体,直到事务完成,锁才会解开,当你想确保数据在连续读期间不被修改,使用它,这种锁模式不会阻碍其它事务读取数据
-
PESSIMISTIC_WRITE
只要事务更新实体,就锁定实体,当多个并发更新事务出现更新失败几率较高时使用这种锁模式,对应上面悲观锁阐述中的
select * from account where name = "yyh" for update
-
PESSIMISTIC_FORCE_INCREMENT
当事务读实体时,就锁定实体,当事务结束时会增加实体的版本属性,即使实体没有修改。
jpa2.0提供了多种方法为实体指定锁模式,如:em
的lock()
和find()
方法指定锁模式。此外EntityManager.refresh()
方法可以恢复实体的状态。
悲观锁实操结果
Hibernate: select user0_.id as id1_3_0_, user0_.age as age2_3_0_, user0_.hiredate as hiredate3_3_0_, user0_.name as name4_3_0_ from User user0_ where user0_.id=? for update
这是单条事务加悲观锁的结果,多条的话,程序会阻塞,因为悲观锁是事务提交后才会解锁。
乐观锁实操结果
Hibernate: update User set age=?, hiredate=?, name=?, version=? where id=? and version=?
Hibernate: select version from User where id =?
Hibernate: update User set age=?, hiredate=?, name=?, version=? where id=? and version=?
javax.persistence.RollbackException: Error while committing the transaction
Caused by: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction
在第二条修改的sql报异常,根据上方的账户余额例子来看符合预期,看图: