学编程,来源栈;先学习,再交钱
当前系列: JDBC&Hibernate 修改讲义

准备一个简单的Student

Student atai = new Student();
atai.setName("阿泰");
atai.setEnroll(LocalDate.now());


EntityManager

要完成entity的持久化,首先需要一个之前提到过的session

  • 按JPA规范,命名为EntityManger
  • 按native传统,命名为Session
EntityManger可以通过EntityMangerFactory的createEntityManager()方法直接获得:
EntityManager em = entityManagerFactory.createEntityManager();

和EntityMangerFactory需要昂贵的开销不同,EntityManger的创建很廉价。而且EntityManger需要追踪entity的状态,不宜共用和长期存活(否则里面的entity会越来越多……),应该像普通对象一样被使用和垃圾回收。

妙的是,可以非常方便的通过EntityManger获得Session对象:

Session session = em.unwrap(Session.class);
PS:这是不是说明native NHibernate一直深入人心


持久化一个entity

对应数据库表中插入一行数据。

使用Session

调用save()方法:
Object id = session.save(atai);
System.out.println(id);
//养成习惯,不要忘了close()
session.close();

会直接将atai保存数据库,并返回数据库生成的id(Object类型,因为会有各种类型的id)

使用EntityManger

没有save()方法,只有persist(),且没有返回值:
em.persist(atai);
System.out.println(atai.getId());
em.close();

试试:能不能用getId()?结果为0

演示:根本没有插入数据!

log4j

Hibernate 5.1表面上使用的是jboss-logging,但实际上jboss-logging只是一个facade(门面),具体的实现还是依赖于log4j等日志组件。

引入log4j2,在log4j2.xml配置文件中添加以下logger:

<Logger name="org.hibernate" level="trace">
log所有各个level指定级别,级别从低到高,依次是:。级别越低,log的信息越详尽,level="debug"就能看到生成的SQL语句:

为了演示方便,我们把日志都输出到文件中:

<AppenderRef ref="File" />
<File name="File" fileName="c:/log/app.log">
	<PatternLayout
		pattern="%d{yyyy-MMM-dd hh:mm:ss}  %level  %n    -%msg%n" />
</File>
fileName指定log文件,PatternLayout指定输出内容/格式。

或者,输出到控制台:

<Console name="Console" target="SYSTEM_OUT">
后期同学们熟练之后,如果只想看到生成的SQL语句,可以使用:
<Logger name="org.hibernate.SQL" level="debug">
如果要看到查询参数和结果的话,再添加一个:
<Logger name="org.hibernate.type.descriptor.sql" level="trace">

另外,如果确实没有log工具又想看到sql语句的话,可以在persistence.xml中配置

<property name="hibernate.show_sql" value="true" /> 
这样SQL语句可以输出到控制台。还有两个辅助配置(对log一样生效)
<!-- 格式化(换行/缩进)SQL语句 -->
<property name="hibernate.format_sql" value="true" />
<!-- 为SQL语句添加注释说明 -->
<property name="hibernate.use_sql_comments" value="true"></property> 

对比session和entityManager,发现调用:

  • session.save(),会导致insert操作的立即(immediately)执行
  • entityManager.persist(),确实把entity添加进session中了,但直到最后也没有执行任何数据库操作,为什么呢?

开启事务

根据JPA规范要求,persist()要完成数据库操作,需要首先开启事务:

EntityTransaction tran = em.getTransaction();
tran.begin();
演示查看log:tran.begin();将transaction由自动开启变成显式开启。

-Preparing to begin transaction via JDBC Connection.setAutoCommit(false)

然后就可以获得生成的id:

try {
	em.persist(atai);
	System.out.println(atai.getId());

但此时事务还未执行完成。(演示:在mysql workbench中还无法查看到插入数据

直到

tran.commit();

被执行,事务才被提交。演示:

  • log中出现:-Transaction committed via JDBC Connection.commit()
  • mysql workbench中查看到插入数据

养成习惯,在catch中回滚事务:

} catch (Exception e) {
	tran.rollback();
}

UUID做主键

当用自增的id做主键时,save()要返回这个id,所以必须进行一次INSERT操作。

但如果id是UUID做主键呢?

我们知道,这时候UUID是由Java/Hibernate生成(演示),这时候还会有即时的INSERT操作么?

断点演示:查看log和workbench

不会进行INSERT操作:

  • 是发生在save()时,
  • 而是flush()时。


取:ById

为了后面的演示方便,我们先来学习通过id(主键)从数据库取entity

SELECT

以下两个方法会通过SELECT语句到数据库查询,用获得的数据填充entity:
  • session
    Student atai = session.get(Student.class, 1);
  • entityManger
    Student atai = em.find(Student.class, 1);

proxy

但这两个方法不会真的查询数据库,而是生成一个只有Id有值的entity的proxy对象(复习:lazy/deferred load

  • session
    Student atai = session.load(Student.class, 1);
  • entityManager
    Student atai = em.getReference(Student.class, 1);

所以注意:不要用通过判断proxy对象是不是null值来判断该id是否存在

if ( atai == null) {
如果atai是load()或者getReference()出来的,它就是一个proxy对象,怎么都不会是null值的!



删除

方法很简单:
session.delete(atai);
em.remove(atai);

关键是:传入的entity怎么来?

标准的做法是从数据库里加载出来的,比如:

Student atai = session.get(Student.class, 1);

但注意:delete()和remove()方法只是在session中的操作,直到session.flush()或者em的transaction.commit(),数据库的DELETE才真正执行。(演示)

性能提升:欺骗失败

你可能会觉得取出entity的SELECT操作是多余的(很多时候确实如此),于是想到了之前学过的laod()/getReference()方法,不会真的SELECT……

#试一试#:居然没用!

Student atai = session.load(Student.class, 1);
session.delete(atai);    //能不能没有SELECT直接DELETE?
为什么呢?因为delete()和remove()是session中对已追踪的entity的操作,但load()和getReference()不会真的生成entity,所以Hibernate delete()的时候在session中找不到相应的entity,只能继续SELECT……

那能不能这样呢?骗一骗Hibernate:

Student atai = new Student();
atai.setId(3);
session.delete(atai);
不行了哟!(以前记得能行)

Exception in thread "main" java.lang.IllegalArgumentException: Removing a detached instance Student#3这样new出来的,而不是session加载进来的,当然是没有被session进行状态追踪管理的,但又有一个id,所以被认为是detached(复习)


更改

@猜一猜@:要执行UPDATE,是不是调用这个方法:

Student atai = session.load(Student.class, 2);
atai.setAge(17);
session.update(atai);    //生成UPDATE语句
session.close();
这是native Hibernate让很多新人懵逼的一个方法。

演示:根本没有生成UPDATE语句

update()方法实际上是对entity进行attach。

#体会:命名的重要性,为什么就不能命名为attach()呢?!#

演示:new出来的Student,被update()之后再delete()就不会抛异常了:

Student atai = new Student();
atai.setId(3);
session.update(atai);
session.delete(atai);
session.flush();
但仔细观察,你会发现出现这里面出现了UPDATE语句!@想一想@:为什么?

转变思想,再次复习session的状态追踪管理和同步

  • 没有专门的生成UPDATE(其实DELETE/INSERT也一样)的方法!
  • 只有一个可以根据entity状态生成相应的UPDATE/DELETE/INSERT语句并执行的flush()方法

#体会:为什么JPA的EntityManager里:

  • 是persist()而不是save()方法,是remove()而不是delete()
  • 没有update()方法只有merge()方法
  • 无论persist()还是remove()都不会访问数据库

就是为了统一/清晰这个概念。

merge()和saveOrUpdate()

EntityManager中的merge()方法,

em.merge(atai);
准确来说,对应的是session中的saveOrUpdate()方法:
session.saveOrUpdate(atai);

本质上就是纳入当前session/em管理,根据entity有无id:

  • 没有id:transient(临时态),会立即通过INSERT获取其id
  • 有id:detached(游离态),会立即通过SELECT获取其信息
建立它的snapshot。(演示:略)


StatelessSession

有时候我们只想快速的完成数据库的增删改查功能,不需要session的状态追踪管理,行不行?

OK的,使用StatelessSession(仅有native支持,没有对应的JPA实现)

//首先拿到SessionFactory
SessionFactory sf = entityManagerFactory.unwrap(SessionFactory.class);
//然后才能通过sf获得
StatelessSession stlSession = sf.openStatelessSession();
然后就可以调用相应的方法了:
Student atai = new Student();
atai.setId(5);		//删改时要设置Id
atai.setName("bo");
stlSession.insert(atai);    //增
stlSession.delete(atai);    //删
stlSession.update(atai);    //改
Student atai = (Student)stlSession.get(Student.class, 5);    //查

注意:

  • 删改都可以自己使用transient的entity
  • 不需要调用flush()方法

演示:

  • 更少的log,没有state track
  • 删改都没有SELECT语句

这样性能就提上来了嘛!^_^

但是,@想一想@:为什么就不干脆都直接使用这种“高性能”的StatelessSession呢?甚至JPA都不提供这种使用方式呢?


#试一试#:new一个entity用StatelessSession进行update


补充强调:要忘了stlSession.close()!对StatelessSession而言,它不仅仅是关闭连接。

演示:只有close()了,事务才被提交

Query    SET autocommit=1


批处理

Hibernate默认是关闭了批处理的。

打开批处理需要在persistence.xml的persistence-unit节点中添加:

<property name="hibernate.jdbc.batch_size" value="5"/>

value值代表批处理中能够包含的最大的语句条数,由开发人员根据实际情况自行调整,官方推荐的是10-50之间。

批处理可以用于增删改查,除了自增int主键表的插入(INSERT)。@想一想@:为什么?

所以Student要改成UUID为主键。

配合StatelessSession

插入20个student:

for (int i = 0; i < 20; i++) {
	Student student = new Student();
	student.setName("s" + i);
	stlSession.insert(student);
}

开启mysql的general_log和Hibernate的log,对比演示:

  • 设置了batch_size,Hibernate的log中会有如下提示,直到到达batch_size,才会有mysql的general_log生成
    Building batch [size=30]
    Reusing batch statement
    Executing batch size: 5
  • 没有设置(其实就是batch_size=1)

有状态追踪Session

当batch_size配合有状态的Session使用时,情况要复杂一些。

首先,何时向数据库发起请求,由flush()决定:即调用flush(),才会发起SQL请求;

然后,flush()的时候,如果其包含的SQL数量:

  • 大于batch_size,会按batch_size数量分批提交执行
  • 小于batch_size,会直接将全部SQL一批次的提交执行(不会说我还等着,下次凑够了再提交执行

所以我们可以将batch_size理解为:一批次提交中最多能包含的SQL语句数量。

但batch_size不能限定session里装多少entity!

如果session里装了太多太多的entity,会给内存造成压力,严重时会触发OutOfMemoryException异常,所以如果数据量大的话,可以用这种方式保证session的大小:

for (int i = 0; i < 20000; i++) {
	Student student = new Student();
	student.setName("s-" + i);
	session.persist(student);
	if (i % 50 == 1) {	//一个session最多追踪50个student 
		session.flush();
		//清空session
		session.clear();
	}
}
session.flush();


dynamicUpdate

在entity的类名上加一行注解:

@DynamicUpdate 
生成UPDATE语句的时候,就会只更改有更改的列:
update
	Student 
set
	age=? 
where
	id=?

而不是所有列都囊括其中。

这样显然能减少ORM和数据库之间传递的内容。尤其是某些列内容非常大(比如文章的正文),但更改的内容有明显和它无关(比如因为点赞更新点赞数量)的时候。

但还是要@想一想@:居然这个功能这么好,为什么不默认/统一使用dynamic update呢?

因为天上不会掉馅饼,生成这种UPDATE语句,是需要把每个属性值和session中的快照进行比对的!

#体会#:性能的问题,绝对不要简单化!


作业

  1. 完成ORM介绍中第3题







学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 JDBC&Hibernate 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码