hibernate和jdbc的渊源

介绍jdbc

咱们学习Java数据库操做时,通常会设计到jdbc的操做,这是一位程序员最基本的素养。jdbc以其优美的代码和高性能,将瞬时态的javabean对象转化为持久态的SQL数据。可是,每次SQL操做都须要创建和关闭链接,这势必会消耗大量的资源开销;若是咱们自行建立链接池,假如每一个项目都这样作,势必搞死的了。同时,咱们将SQL语句预编译在PreparedStatement中,这个类可使用占位符,避免SQL注入,固然,后面说到的hibernate的占位符的原理也是这样,同时,mybatis的#{}占位符原理也是如此。预编译的语句是原生的SQL语句,好比更新语句:php

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操做html

于是,咱们在确定它的优势时,也不该该规避他的缺点。随着工业化步伐的推动,每一个数据库每每涉及到不少表,每张表有可能会关联到其余表,若是咱们仍是按照jdbc的方式操做,这无疑是增长了开发效率。因此,有人厌倦这种复杂繁琐、效率低下的操做,因而,写出了著名的hibernate框架,封装了底层的jdbc操做,如下是jdbc的优缺点:前端

jdbc的有点和缺点

由上图能够看见,jdbc不适合公司的开发,公司毕竟以最少的开发成原本创造更多的利益。这就出现了痛点,商机伴随着痛点的出现。于是,应世而生了hibernate这个框架。即使没有hibernate的框架,也会有其余框架生成。hibernate的底层封装了jdbc,好比说jdbc为了防止sql注入,通常会有占位符,hibernate也会有响应的占位符。hibernate是orm(object relational mapping)的一种,即对象关系映射。vue


对象关系映射

通俗地来讲,对象在pojo中能够指Javabean,关系能够指MySQL的关系型数据库的表字段与javabean对象属性的关系。映射能够用咱们高中所学的函数映射,即Javabean瞬时态的对象映射到数据库的持久态的数据对象。咱们都知道javabean在内存中的数据是瞬时状态或游离状态,就像是宇宙星空中的一颗颗行星,除非行星被其余行星所吸引,才有可能不成为游离的行星。瞬时状态(游离状态)的javabean对象的生命周期随着进程的关闭或者方法的结束而结束。若是当前javabean对象与gcRoots没有直接或间接的关系,其有可能会被gc回收。咱们就没办法长久地存储数据,这是一个很是头疼的问题。假如咱们使用文件来存储数据,可是文件操做起来很是麻烦,并且数据格式不是很整洁,不利于后期的维护等。于是,横空出世了数据库。咱们可使用数据库存储数据,数据库中的数据才是持久数据(数据库的持久性),除非人为的删除。这里有个问题——怎么将瞬时状态(游离状态)的数据转化为持久状态的数据,确定须要一个链接Javabean和数据库的桥梁,因而乎就有了上面的jdbc。java

单独来讲mysql,mysql是一个远程服务器。咱们在向mysql传输数据时,并非对象的方式传输,而是以字节码的方式传输数据。为了保证数据准确的传输,咱们通常会序列化当前对象,用序列号标志这个惟一的对象。若是,咱们不想存储某个属性,它是有数据库中的数据拼接而成的,咱们大可不用序列化这个属性,可使用Transient来修饰。好比下面的获取图片的路径,其实就是服务器图片的文件夹地址和图片的名称拼接而成的。固然,你想存储这个属性,也能够存储这个属性。咱们有时候图片的路由的字节很长,这样会占用MySQL的内存。于是,学会取舍,何尝不是一个明智之举。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并不懂技术,他们只是照本宣科的提问。若是你照本宣科的回答,它们觉着你很厉害。可是,若是是一个懂技术的人提问你,若是你只是临时背了它们的区别,而没有相应的工做经验,他们会问的让你手足无措。web


hibernate讲解

由于咱们公司使用的是hibernate,我在这里简单地介绍下hibernate。但相对于jdbc来讲,hibernate框架仍是比较重的。为何说他重,由于它集成了太多的东西,看以下的hibernate架构图:面试

hibernate_architecture.jpg

你会发现最上层使咱们的java应用程序的开始,好比web的Tomcat服务器的启动,好比main方法的启动等。紧接着就是须要(needing)持久化的对象,这里为何说是须要,而不是持久化的对象。只有保存到文件、数据库中的数据才是持久化的。想经过hibernate,咱们能够绝不费力的将瞬时状态的数据转化为持久状态的数据,下面即是hibernate的内部操做数据。其通常是这样的流程:spring

  • 我我的画的

hibernate内部流程

1055646-20170616114130884-2134062939.png

若是你是用过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为何重(zhong)?

好比咱们执行插入语句,可使用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的存储引擎。


innodb存储引擎

咱们都知道这两种存储引擎的区别,若是不知道的话,能够参这篇文章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的返回值都是一个持久化的实例对象,但对于参数而言不会改变它的状态。

相关文章
相关标签/搜索