乐趣区

hibernate和jdbc的渊源

简单介绍 jdbc
我们学习 Java 数据库操作时,一般会设计到 jdbc 的操作,这是一位程序员最基本的素养。jdbc 以其优美的代码和高性能,将瞬时态的 javabean 对象转化为持久态的 SQL 数据。但是,每次 SQL 操作都需要建立和关闭连接,这势必会消耗大量的资源开销;如果我们自行创建连接池,假如每个项目都这样做,势必搞死的了。同时,我们将 SQL 语句预编译在 PreparedStatement 中,这个类可以使用占位符,避免 SQL 注入,当然,后面说到的 hibernate 的占位符的原理也是这样,同时,mybatis 的 #{} 占位符原理也是如此。预编译的语句是原生的 SQL 语句,比如更新语句:
private static int update(Student student) {
Connection conn = getConn();
int i = 0;
String sql = “update students set Age='” + student.getAge() + “‘ where Name='” + student.getName() + “‘”;
PreparedStatement pstmt;
try {
pstmt = (PreparedStatement) conn.prepareStatement(sql);
i = pstmt.executeUpdate();
System.out.println(“resutl: ” + i);
pstmt.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
return i;
}
上面的 sql 语句没有使用占位符,如果我们少了 varchar 类型的单引号,就会保存失败。在这种情况下,如果写了很多句代码,最后因为一个单引号,导致代码失败,对于程序员来说,无疑是很伤自信心的事。如果涉及到了事务,那么就会非常的麻烦,一旦一个原子操作的语句出现错误,那么事务就不会提交,自信心就会跌倒低谷。然而,这仅仅是更新语句,如果是多表联合查询语句,那么就要写很多代码了。具体的 jdbc 的操作,可以参考这篇文章:jdbc 操作。
因而,我们在肯定它的优点时,也不应该规避他的缺点。随着工业化步伐的推进,每个数据库往往涉及到很多表,每张表有可能会关联到其他表,如果我们还是按照 jdbc 的方式操作,这无疑是增加了开发效率。所以,有人厌倦这种复杂繁琐、效率低下的操作,于是,写出了著名的 hibernate 框架,封装了底层的 jdbc 操作,以下是 jdbc 的优缺点:

由上图可以看见,jdbc 不适合公司的开发,公司毕竟以最少的开发成本来创造更多的利益。这就出现了痛点,商机伴随着痛点的出现。因而,应世而生了 hibernate 这个框架。即便没有 hibernate 的框架,也会有其他框架生成。hibernate 的底层封装了 jdbc,比如说 jdbc 为了防止 sql 注入,一般会有占位符,hibernate 也会有响应的占位符。hibernate 是 orm(object relational mapping)的一种,即对象关系映射。
什么对象关系映射
通俗地来说,对象在 pojo 中可以指 Javabean,关系可以指 MySQL 的关系型数据库的表字段与 javabean 对象属性的关系。映射可以用我们高中所学的函数映射,即 Javabean 顺时态的对象映射到数据库的持久态的数据对象。我们都知道 javabean 在内存中的数据是瞬时状态或游离状态,就像是宇宙星空中的一颗颗行星,除非行星被其他行星所吸引,才有可能不成为游离的行星。瞬时状态(游离状态)的 javabean 对象的生命周期随着进程的关闭或者方法的结束而结束。如果当前 javabean 对象与 gcRoots 没有直接或间接的关系,其有可能会被 gc 回收。我们就没办法长久地存储数据,这是一个非常头疼的问题。假如我们使用文件来存储数据,但是文件操作起来非常麻烦,而且数据格式不是很整洁,不利于后期的维护等。因而,横空出世了数据库。我们可以使用数据库存储数据,数据库中的数据才是持久数据(数据库的持久性),除非人为的删除。这里有个问题——怎么将瞬时状态(游离状态)的数据转化为持久状态的数据,肯定需要一个连接 Javabean 和数据库的桥梁,于是乎就有了上面的 jdbc。
单独来说 mysql,mysql 是一个远程服务器。我们在向 mysql 传输数据时,并不是对象的方式传输,而是以字节码的方式传输数据。为了保证数据准确的传输,我们一般会序列化当前对象,用序列号标志这个唯一的对象。如果,我们不想存储某个属性,它是有数据库中的数据拼接而成的,我们大可不用序列化这个属性,可以使用 Transient 来修饰。比如下面的获取图片的路径,其实就是服务器图片的文件夹地址和图片的名称拼接而成的。当然,你想存储这个属性,也可以存储这个属性。我们有时候图片的路由的字节很长,这样会占用 MySQL 的内存。因而,学会取舍,未尝不是一个明智之举。
@Entity
@Table(name = “core_picture”)
public class Picture extends BaseTenantObj {

。。。。

@Transient
public String getLocaleUrl() {
return relativeFolder.endsWith(“/”) ? relativeFolder + name : relativeFolder + “/” + name;
}

。。。。

}
网上流传盛广的对象关系映射的框架(orm)有 hibernate、mybatis 等。重点说说 hibernate 和 mybatis 吧。hibernate 是墨尔本的一位厌倦重复的 javabean 的程序员编写而成的,mybatis 是 appache 旗下的一个子产品,其都是封装了 jdbc 底层操作的 orm 框架。但 hibernate 更注重 javabean 与数据表之间的关系,比如我们可以使用注解生成数据表,也可以通过注解的方式设置字段的类型、注释等。他将 javabean 分成了游离态、顺时态、持久态等,hibernate 根据这三种状态来触及 javabean 对象的数据。而 mybatis 更多的是注重 SQL 语句的书写,也就是说主要是 pojo 与 SQL 语句的数据交互,对此,Hibernate 对查询对象有着良好的管理机制,用户无需关心 SQL。一旦 SQL 语句的移动,有可能会影响字段的不对应,因而,mybatis 移植性没有 hibernate 好。mybatis 接触的是底层 SQL 数据的书写,hibernate 根据 javabean 的参数来生成 SQL 语句,再将 SQL 查询的结果封装成 pojo,因而,mybatis 的性能相来说优于 hibernate,但这也不是绝对的。性能还要根据你的表的设计结构、SQL 语句的封装、网络、带宽等等。我只是抛砖引玉,它们具体的区别,可以参考这篇文档。mybatis 和 hibernate 的优缺点。
hibernate 和 mybatis 之间的区别,也是很多公司提问面试者的问题。但是真正熟知他们区别的人,一般是技术选型的架构师。如果你只是负责开发,不需要了解它们的区别,因为他们都封装了 jdbc。所以,你不论使用谁,都还是比较容易的。然而,很多公司的 HR 提问这种问题很死板,HR 并不懂技术,他们只是照本宣科的提问。如果你照本宣科的回答,它们觉着你很厉害。但是,如果是一个懂技术的人提问你,如果你只是临时背了它们的区别,而没有相应的工作经验,他们会问的让你手足无措。
hibernate 的讲解
因为我们公司使用的是 hibernate,我在这里简单地介绍下 hibernate。但相对于 jdbc 来说,hibernate 框架还是比较重的。为什么说他重,因为它集成了太多的东西,看如下的 hibernate 架构图:

你会发现最上层使我们的 java 应用程序的开始,比如 web 的 Tomcat 服务器的启动,比如 main 方法的启动等。紧接着就是需要(needing)持久化的对象,这里为什么说是需要,而不是持久化的对象。只有保存到文件、数据库中的数据才是持久化的想通过 hibernate,我们可以毫不费力的将瞬时状态的数据转化为持久状态的数据,下面便是 hibernate 的内部操作数据。其一般是这样的流程:
我个人画的

这个地址的图片

如果你是用过 jdbc 连接数据库的话,我们一般是这样写的:
package com.zby.jdbc.config;

import com.zby.util.exception.TableException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

/**
* Created By zby on 22:12 2019/1/5
*/

public class InitJdbcFactory {

private static Properties properties = new Properties();

private static Logger logger = LoggerFactory.getLogger(InitJdbcFactory.class);

static {
try {
// 因为使用的类加载器获取配置文件,因而,配置文件需要放在 classpath 下面,
// 方能读到数据
properties.load(Thread.currentThread().getContextClassLoader().
getResourceAsStream(“./jdbc.properties”));
} catch (IOException e) {
logger.info(“ 初始化 jdbc 失败 ”);
e.printStackTrace();
}
}

public static Connection createConnection() {
String drivers = properties.getProperty(“jdbc.driver”);
if (StringUtils.isBlank(drivers)) {
drivers = “com.mysql.jdbc.Driver”;
}
String url = properties.getProperty(“jdbc.url”);
String username = properties.getProperty(“jdbc.username”);
String password = properties.getProperty(“jdbc.password”);
try {
Class.forName(drivers);
return DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
logger.error(InitColTable.class.getName() + “:连接数据库的找不到驱动类 ”);
throw new TableException(InitColTable.class.getName() + “: 连接数据库的找不到驱动类 ”, e);
} catch (SQLException e) {
logger.error(InitColTable.class.getName() + “:连接数据库的 sql 异常 ”);
throw new TableException(InitColTable.class.getName() + “ 连接数据库的 sql 异常 ”, e);
}
}
}

hibernate 一般这样连接数据库:
public class HibernateUtils {

private static SessionFactory sf;

// 静态初始化
static{

//【1】加载配置文件
Configuration conf = new Configuration().configure();

//【2】根据 Configuration 配置信息创建 SessionFactory
sf = conf.buildSessionFactory();

// 如果这里使用了 hook 虚拟机,需要关闭 hook 虚拟机
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(“ 虚拟机关闭! 释放资源 ”);
sf.close();
}
}));

}

/**
* 采用 openSession 创建一个与数据库的连接会话,但这种方式需要手动关闭与数据库的 session 连接(会话),
* 如果不关闭,则当前 session 占用数据库的资源无法释放,最后导致系统崩溃。
*
**/
public static org.hibernate.Session openSession(){

//【3】获得 session
Session session = sf.openSession();
return session;
}

/**
* 这种方式连接数据库,当提交事务时,会自动关闭当前会话;
* 同时,创建 session 连接时,autoCloseSessionEnabled 和 flushBeforeCompletionEnabled 都为 true,
* 并且 session 会同 sessionFactory 组成一个 map 以 sessionFactory 为主键绑定到当前线程。
* 采用 getCurrentSession() 需要在 Hibernate.cfg.xml 配置文件中加入如下配置:
如果是本地事物,及 JDBC 一个数据库:
<propety name=”Hibernate.current_session_context_class”>thread</propety>
如果是全局事物,及 jta 事物、多个数据库资源或事物资源:
<propety name=”Hibernate.current_session_context_class”>jta</propety>
使用 spring 的 getHiberanteTemplate 就不需要考虑事务管理和 session 关闭的问题:
*
**/
public static org.hibernate.Session getCurrentSession(){
//【3】获得 session
Session session = sf.getCurrentSession();
return session;
}
}
mybatis 的配置文件:
public class DBTools {
public static SqlSessionFactory sessionFactory;

static{
try {
// 使用 MyBatis 提供的 Resources 类加载 mybatis 的配置文件
Reader reader = Resources.getResourceAsReader(“mybatis.cfg.xml”);
// 构建 sqlSession 的工厂
sessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (Exception e) {
e.printStackTrace();
}
}

// 创建能执行映射文件中 sql 的 sqlSession
public static SqlSession getSession(){
return sessionFactory.openSession();
}
}
hibernate、mybatis、jdbc 创建于数据库的连接方式虽然不同,但最红都是为了将顺时态的数据写入到数据库中的,但这里主要说的是 hibernate。但是 hibernate 已经封装了这些属性,我们可以在 configuration 在配置驱动类、用户名、用户密码等。再通过 sessionFactory 创建 session 会话,也就是加载 Connection 的物理连接,创建 sql 的事务,然后执行一系列的事务操作,如果事务全部成功即可成功,但反有一个失败都会失败。jdbc 是最基础的操作,但是,万丈高楼平地起,只有基础打牢,才能走的更远。因为 hibernate 封装了这些基础,我们操作数据库不用考虑底层如何实现的,因而,从某种程度上来说,hibernate 还是比较重的。
hibernate 为什么会重?
比如我们执行插入语句,可以使用 save、saveOrUpdate,merge 等方法。需要将实体 bean 通过反射转化为 mysql 的识别的 SQL 语句,同时,查询虽然用到了反射,但是最后转化出来的还是 object 的根对象,这时需要将根对象转化为当前对象,返回给客户端。虽然很笨重,但是文件配置好了,可以大大地提高开发效率。毕竟现在的服务器的性能都比较好,公司追求的是高效率的开发,而往往不那么看重性能,除非用户提出性能的问题。
说说 merge 和 saveOrUpdate
merge 方法与 saveOrUpdate 从功能上类似,但他们仍有区别。现在有这样一种情况:我们先通过 session 的 get 方法得到一个对象 u,然后关掉 session,再打开一个 session 并执行 saveOrUpdate(u)。此时我们可以看到抛出异常:Exception in thread “main” org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session,即在 session 缓存中不允许有两个 id 相同的对象。不过若使用 merge 方法则不会异常,其实从 merge 的中文意思(合并)我们就可以理解了。我们重点说说 merge。merge 方法产生的效果和 saveOrUpdate 方法相似。这是 hibernate 的原码:
void saveOrUpdate(Object var1);

void saveOrUpdate(String var1, Object var2);

public Object merge(Object object);

public Object merge(String var1, Object var2);
前者不用返回任何数据,后者返回的是持久化的对象。如果根据 hibernate 的三种状态,比如顺时态、持久态、游离态来说明这个问题,会比较难以理解,现在,根据参数有无 id 或 id 是否已经存在来理解 merge,而且从执行他们两个方法而产生的 sql 语句来看是一样的。

参数实例对象没有提供 id 或提供的 id 在数据库找不到对应的行数据,这时 merger 将执行插入操作吗,产的 SQL 语句如下:
Hibernate: select max(uid) from user

Hibernate: insert into hibernate1.user (name, age, uid) values (?, ?, ?)

一般情况下,我们新 new 一个对象,或者从前端向后端传输 javabean 序列化的对象时,都不会存在当前对象的 id,如果使用 merge 的话,就会向数据库中插入一条数据。
参数实例对象的 id 在数据库中已经存在,此时又有两种情况
(1)如果对象有改动,则执行更新操作,产生 sql 语句有:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=?
Hibernate: update hibernate1.user set name=?, age=? where uid=?

(2)如果对象未改动,则执行查询操作,产生的语句有:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=?

以上三种是什么情况呢?如果我们保存用户时,数据库中肯定不存在即将添加的用户,也就是说,我们的保存用户就是向数据库中添加用户。但是,其也会跟着某些属性,比如说用户需要头像,这是多对一的关系,一个用户可能多个对象,然而,头像的关联的 id 不是放在用户表中的,而是放在用户扩张表中的,这便用到了切分表的概念。题外话,我们有时会用到快照表,比如商品快照等,也许,我们购买商品时,商品是一个价格,但随后商品的价格变了,我们需要退商品时,就不应该用到商品改变后的价格了,而是商品改变前的价格。扩展表存放用户额外的信息,也就是用户非必须的信息,比如说昵称,性别,真实姓名,头像等。因而,头像是图片类型,使用 hibernate 的注解方式,创建用户表、图片表、用户扩展表。如下所示(部分重要信息已省略)
// 用户头像
@Entity
@Table(name = “core_user”)
public class User extends BaseTenantConfObj {

/**
* 扩展表
* */
@OneToOne(mappedBy = “user”, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private UserExt userExt;

}

// 用户扩展表的头像属性
@Entity
@Table(name = “core_user_ext”)
public class UserExt implements Serializable {

/**
* 头像
*/
@ManyToOne
@JoinColumn(name = “head_logo”)
private Picture headLogo;

}

// 图片表
@Entity
@Table(name = “core_picture”)
public class Picture extends BaseTenantObj {
private static Logger logger = LoggerFactory.getLogger(Picture.class);

。。。。。。
// 图片存放在第三方的相对 url。
@Column(name = “remote_relative_url”, length = 300)
private String remoteRelativeUrl;

// 图片大小
@Column(length = 8)
private Integer size;

/**
* 图片所属类型
* user_logo: 用户头像
*/
@Column(name = “host_type”, length = 58)
private String hostType;

// 照片描述
@Column(name = “description”, length = 255)
private String description;
}

前端代码是:
// 这里使用到了 vue.js 的代码,v-model 数据的双向绑定,前端的 HTML 代码
<tr>
<td class=”v-n-top”> 头像:</td>
<td>
<div class=”clearfix”>
<input type=”hidden” name=”headLogo.id” v-model=”pageData.userInfo.logo.id”/>
<img class=”img-user-head fl” :src=”(pageData.userInfo&&pageData.userInfo.logo&&pageData.userInfo.logo.path) ? (constant.imgPre + pageData.userInfo.logo.path) : ‘img/user-head-default.png'”>
<div class=”img-btn-group”>
<button cflag=”upImg” type=”button” class=”btn btn-sm btn-up”> 点击上传 </button>
<button cflag=”delImg” type=”button” class=”btn btn-white btn-sm btn-del”> 删除 </button>
</div>
</div>
<p class=”img-standard”> 推荐尺寸 800*800;支持.jpg, .jpeg, .bmp, .png 类型文件,1M 以内 </p>
</td>
</tr>

// 这里用到了 js 代码,这里用到了 js 的属性方法
upImg: function(me) {
Utils.asyncImg({
fn: function(data) {
vm.pageData.userInfo.logo = {
path: data.remoteRelativeUrl,
id: data.id
};
}
});
},
上传头像是异步提交,如果用户上传了头像,我们在提交用户信息时,通过“headLogo.id”可以获取当前头像的持久化的图片对象,hibernate 首先会根据属性 headLogo 找到图片表,根据当前头像的 id 找到图片表中对应的行数据,为什么可以根据 id 来获取行数据?
- 图片表的表结构信息

从这张图片可以看出,图片采用默认的存储引擎,也就是 InnoDB 存储引擎,而不是 myiSam 的存储引擎。我们都知道这两种存储引擎的区别,如果不知道的话,可以参这篇文章 MySQL 中 MyISAM 和 InnoDB 的索引方式以及区别与选择。innodb 采用 BTREE 树的数据结构方式存储,它有 0 到 1 直接前继和 0 到 n 个直接后继,这是什么意思呢?一棵树当前叶子节点的直接父节点只有一个,但其儿子节点可以一个都没有,也可以有 1 个、2 个、3 个 ……, 如果 mysql 采用的多对一的方式存储的话,你就会发现某条外键下有许多行数据,比如如下的这张表

这张表记录的是项目的完成情况,一般有预约阶段,合同已签,合同完成等等。你会发现 project_id=163 的行数据不止一条,我们通过查询语句:SELECT zpp.* from zq_project_process zpp WHERE zpp.is_deleted = 0 AND zpp.project_id=163,查找速度非常快。为什么这么快呢,因为我刚开始说的 innodb 采用的 BTREE 树结构存储,其数据是放在当前索引下,什么意思?innodb 的存储引擎是以索引作为当前节点值,比如说银行卡表的有个主键索引,备注,如果我们没有创建任何索引,如果采用的 innodb 的数据引擎,其内部会创建一个默认的行索引,这就像我们在创建 javabean 对象时,没有创建构造器,其内部会自动创建一个构造器的道理是一样的。其数据是怎么存储的呢,如下图所示:
mysql 银行卡数据

其内部存储数据

其所对应的行数据是放在当前索引下的,因而,我们取数据不是取表中中的数据,而是取当前主键索引下的数据。项目进程表如同银行卡的主键索引,只不过其有三个索引,分别是主键索引和两个外键索引,如图所示的索引:

索引名是 hibernate 自动生成的一个名字,索引是项目 id、类型两个索引。因为我们不是从表中取数据,而是从当前索引的节点下取数据,所以速度当然快了。索引有主键索引、外键索引、联合索引等,但一般情况下,主键索引和外键索引使用频率比较高。同时,innodb 存储引擎的支持事务操作,这是非常重要,我们操作数据库,一般都是设计事务的操作,这也 mysql 默认的存储引擎是 innodb。
我们通过主键获取图片的行数据,就像通过主键获取银行卡的行数据。这也是上面所说的,根据是否有 id 来确定是插入还是更新数据。通过图片主键 id 获取该行数据后,hibernate 会在堆中创建一个 picture 对象。用户扩展表的 headLogo 属性指向这个图片对象的首地址,从而创建一个持久化的图片对象。前台异步提交头像时,如果是编辑头像,hibernate 会觉擦到当前对象的属性发生了改变,于是,在提交事务时将修改后的游离态的类保存到数据库中。如果我们保存或修改用户时,我们保存的就是持久化的对象,其内部会自动存储持久化头像的 id。这是 hibernate 底层所做的,我们不需要关心。
再举一个 hibernate 事务提交的例子:
我们在支付当中搞得提现事务时,调用第三方支付的 SDK 时,第三方一般会用我们到订单号,比如我们调用连连支付这个第三方支付的 SDK 的 payRequestBean 的实体类:
/**
* Created By zby on 11:00 2018/12/11
* 发送到连连支付的 body 内容
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PaymentRequestBean extends BaseRequestBean {

/**
* 版本号
*/
@NonNull
private String api_version;

/**
* 银行账户
*/
@NonNull
private String card_no;

/**
* 对私
*/
@NonNull
private String flag_card;

/**
* 回调接口
*/
@NonNull
private String notify_url;

/**
* 商户订单号
*/
@NonNull
private String no_order;

/**
* 商户订单时间,时间格式为 YYYYMMddHHmmss
*/
@NonNull
private String dt_order;

/**
* 交易金额
*/
@NonNull
public String money_order;

/**
* 收款方姓名 即账户名
*/
@NonNull
private String acct_name;

/**
* 收款银行姓名
*/
private String bank_name;

/**
* 订单描述 , 代币类型 + 支付
*/
@NonNull
private String info_order;

/**
* 收款备注
*/
private String memo;

/**
* 支行名称
*/
private String brabank_name;

}
商户订单号是必传的,且这个订单号是我们这边提供的,这就有一个问题了,怎么避免订单号不重复呢?我们可以在提现记录表事先存储一个订单号,订单号的规则如下:”WD” + 系统时间 + 当前提现记录的 id,这个 id 怎么拿到呢?既然底层使用的是 merge 方法,我们事先不创建订单号,先保存这个记录,其返回的是已经创建好的持久化的对象,该持久化的对象肯定有提现主键的 id。我们拿到该持久化对象的主键 id,便可以封装订单号,再次保存这个持久化的对象,其内部会执行类似以下的操作:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_
from hibernate1.user user0_ where user0_.uid=?
Hibernate: update hibernate1.user set name=?, age=? where uid=?
代码如下:
withdraw.setWithdrawStatus(WITHDRAW_STATUS_WAIT_PAY);
withdraw.setApplyTime(currentTime);
withdraw.setExchangeHasThisMember(hasThisMember ? YES : NO);
withdraw = withdrawDao.save(withdraw);
withdraw.setOrderNo(“WD” + DateUtil.ISO_DATETIME_FORMAT_NONE.format(currentTime) + withdraw.getId());
withdrawDao.save(withdraw);
不管哪种情况,merge 的返回值都是一个持久化的实例对象,但对于参数而言不会改变它的状态。

退出移动版