我们学习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。但相对于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还是比较重的。
比如我们执行插入语句,可以使用save、saveOrUpdate,merge等方法。需要将实体bean通过反射转化为mysql的识别的SQL语句,同时,查询虽然用到了反射,但是最后转化出来的还是object的根对象,这时需要将根对象转化为当前对象,返回给客户端。虽然很笨重,但是文件配置好了,可以大大地提高开发效率。毕竟现在的服务器的性能都比较好,公司追求的是高效率的开发,而往往不那么看重性能,除非用户提出性能的问题。
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的话,就会向数据库中插入一条数据。
(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对象时,没有创建构造器,其内部会自动创建一个构造器的道理是一样的。 其数据是怎么存储的呢,如下图所示:
其所对应的行数据是放在当前索引下的,因而,我们取数据不是取表中中的数据,而是取当前主键索引下的数据。项目进程表如同银行卡的主键索引,只不过其有三个索引,分别是主键索引和两个外键索引,如图所示的索引:
索引名是hibernate自动生成的一个名字,索引是项目id、类型两个索引。因为我们不是从表中取数据,而是从当前索引的节点下取数据,所以速度当然快了。 索引有主键索引、外键索引、联合索引等,但一般情况下,主键索引和外键索引使用频率比较高。 同时,innodb存储引擎的支持事务操作,这是非常重要,我们操作数据库,一般都是设计事务的操作,这也mysql默认的存储引擎是innodb。
我们通过主键获取图片的行数据,就像通过主键获取银行卡的行数据。这也是上面所说的,根据是否有id来确定是插入还是更新数据。通过图片主键id获取该行数据后,hibernate会在堆中创建一个picture对象。用户扩展表的headLogo属性指向这个图片对象的首地址,从而创建一个持久化的图片对象。前台异步提交头像时,如果是编辑头像,hibernate会觉擦到当前对象的属性发生了改变,于是,在提交事务时将修改后的游离态的类保存到数据库中。如果我们保存或修改用户时,我们保存的就是持久化的对象,其内部会自动存储持久化头像的id。这是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);