Mybaits系列之MyBatis的发展之路,怎么用好MyBatis

为什么要用Mybatis?

先看看原生操作JDBC的步骤

*注册驱动,获取连接

*创建StateMent对象

*execute()方法执行SQL

*把结果集转换成POJO

*关闭资源

一看,存在大量的重复代码,繁琐过程,结果集的处理很复杂,数据库连接的管理也很麻烦。

所以,慢慢就出现了一些包装数据库操作的框架,springJDBC、dbUtils、heibernate、mybatis等。

先说说比较早的springJDBC和dbUtil,它们主要解决了数据源的封装和映射结果集的包装。

但是并没有真正解决SQL语句的硬编码(直接把sql写在代码中,业务操作和数据库操作耦合在一期);参数只能按顺序传入(占位符),不能就通过一个对象传进去来操作;没有实现实体类到数据库记录的映射;没有提供缓存等功能。

为了解决这些问题,真正的ORM级的框架就出现了。

Hibernate(全自动化,jpa是对hibernate的封装):

hbm.xml或者实体类注解(class对应表,字段对应列)

提供session对象来直接操作对象,比如session.save(bo) 

问题:

1、不能指定部分字段(不能像sql样只查部分)

2、无法自定义SQL,优化困难

3、不支持动态SQL

MyBatis(半自动化):

1、使用连接池对连接进行管理

2、SQL和代码分离,集中管理

3、参数映射和动态和动态SQL

4、结果集映射

5、缓存管理

6、重复SQL的提取,sql的标签,可以在其它地方来引用

7、插件机制

 

实际开发建议:

*业务简单的项目可以使用Hibernate

*需要灵活的SQL,可以使用,MyBatis

*对性能要求高,可以使用JDBC

*Spring JDBC可以和ORM框架混用

 

MyBatis编程式开发核心配置

去除Spring容器,我们就用java和Mybatis来耦合,看看纯MyBatis是怎么编程的。

1、mybatis和MySQL.jar包依赖

2、全局配置文件mybatis-config.xml

3、映射器Mapper.xml

4、Mapper接口

  /**
     * 使用MyBatis API方式
     * @throws IOException
     */
    @Test
    public void testStatement() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession();
        try {
            Blog blog = (Blog) session.selectOne("com.darker.mapper.BlogMapper.selectBlogById", 1);
            System.out.println(blog);
        } finally {
            session.close();
        }
    }

/**
     * 通过 SqlSession.getMapper(XXXMapper.class)  接口方式
     * @throws IOException
     */
    @Test
    public void testSelect() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
        try {
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            Blog blog = mapper.selectBlogById(1);
            System.out.println(blog);
        } finally {
            session.close();
        }
    }

 

四个核心对象的生命周期

SqlSessionFactoryBuiler  创建工厂类(产生了工厂类对象就可以丢弃,方法内部)

SqlSessionFactory   创建会话(需要一直存在,不停的创建会话,应该是单例一直存在的)

SqlSession  会话(一个请求的方法里面存在)

Mapper映射(单个方法)

 

mybatis-config.xml(全局配置文件详解)

首先看到我们的xml,发现有个root的标签configuration,既然有这个根标签,马上就能想到MyBatis中是不是会有个Configruation配置类。

发现了,果然有这个配置类,所以一直说配置源于代码,比如spring的各种配置一样也能在源码中找到(好多小伙伴一直感觉各个版本的配置标签不统一,可能会有更改,不好查询,其实直接看源码就可以),下面我们就来好好分析下这些标签。

一级标签:

*properties,把一些需要重复引用的属性放到这个标签中, 比如数据库连接得配置。

*settings,mybatis核心行为得控制。

 *typeAliases别名配置,简化类型全路径得拼写(一般会换成包扫描)

*typeHandler,类型处理器,为什么mybatis可以把数据库得varchar类型对应到我们java的string类型

它也支持自定义

<result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.datker.type.MyTypeHandler"/>
public class MyTypeHandler extends BaseTypeHandler<String> {
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
            throws SQLException {
        // 设置 String 类型的参数的时候调用,Java类型到JDBC类型
        // 注意只有在字段上添加typeHandler属性才会生效
        // insertBlog name字段
        System.out.println("---------------setNonNullParameter1:"+parameter);
        ps.setString(i, parameter);
    }

    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 根据列名获取 String 类型的参数的时候调用,JDBC类型到java类型
        // 注意只有在字段上添加typeHandler属性才会生效
        System.out.println("---------------getNullableResult1:"+columnName);
        return rs.getString(columnName);
    }

    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        // 根据下标获取 String 类型的参数的时候调用
        System.out.println("---------------getNullableResult2:"+columnIndex);
        return rs.getString(columnIndex);
    }

    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        System.out.println("---------------getNullableResult3:");
        return cs.getString(columnIndex);
    }
}

*objectFactory,对象工厂,通过反射创建java对象。

*plugin,允许你在映射语句执行过程中的某一点进行拦截调用。

*environments,配置环境以及里面的熟悉,数据源,事务等。

*mappers,映射器,定义 SQL 映射语句。

 

Mapper中内含的八大标签

<cache>

<cache-ref>

<resultMap>

<sql>,提取重复被使用的sql语句。

<insert>

<update>

<delete>

<select>

 

MyBatis编程式开发最佳实践

动态SQL配置(解决动态sql生成的问题)

if标签的使用

 <select id="selectByMap" parameterType="java.util.Map" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        FROM tbl_dept e
        where 1=1
        <if test="did != null">
            and d_id = #{did,jdbcType=INTEGER}
        </if>
    </select>

choose(when,otherwise)标签的使用

 <select id="getEmpList_choose" resultMap="empResultMap" parameterType="com.darker.crud.bean.Employee">
        SELECT * FROM tbl_emp e
        <where>
            <choose>
                <when test="empId !=null">
                    e.emp_id = #{emp_id, jdbcType=INTEGER}
                </when>
                <when test="empName != null and empName != ''">
                    AND e.emp_name LIKE CONCAT(CONCAT('%', #{emp_name, jdbcType=VARCHAR}),'%')
                </when>
                <when test="email != null ">
                    AND e.email = #{email, jdbcType=VARCHAR}
                </when>
                <otherwise>
                </otherwise>
            </choose>
        </where>
    </select>

 trim(where,set)标签的使用

 <!--有选择的插入-->
    <insert id="insertSelective" parameterType="com.darker.crud.bean.Employee">
        insert into tbl_emp
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="empId != null">
                emp_id,
            </if>
            <if test="empName != null">
                emp_name,
            </if>
            <if test="gender != null">
                gender,
            </if>
            <if test="email != null">
                email,
            </if>
            <if test="dId != null">
                d_id,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="empId != null">
                #{empId,jdbcType=INTEGER},
            </if>
            <if test="empName != null">
                #{empName,jdbcType=VARCHAR},
            </if>
            <if test="gender != null">
                #{gender,jdbcType=CHAR},
            </if>
            <if test="email != null">
                #{email,jdbcType=VARCHAR},
            </if>
            <if test="dId != null">
                #{dId,jdbcType=INTEGER},
            </if>
        </trim>
    </insert>

foeach 标签的使用,批量操作比java层面节省性能

<!-- List 批量删除  -->
    <delete id="deleteByList" parameterType="java.util.List">
        delete from tbl_emp where emp_id in
        <foreach collection="list" item="item" open="(" separator="," close=")">
            #{item.empId,jdbcType=VARCHAR}
        </foreach>
    </delete>

 

 批量操作执行器

上面动态标签foreach可以批量操作,但如果出现要批量删除或者新增非常大量的数据,就会导致这个sql可能会非常长,而MyBatis对这种sql的大小是有限制的,默认是4m,那么这种情况应该怎么解决呢。

这里就需要用到MyBatis里面提供的批量操作执行器,在xml中的<settings>标签可以配置

simple:默认,对应Statement。

reuse:可重用的执行器,就是会把sql缓存起来,第二次用就会再去拿到,对应PrepareStatement。

batch:可重用,且还会执行批量操作,依然是对应PrepareStatement。

在JDBC代码层面对应

   /**
     * 原生JDBC的批量操作方式 ps.addBatch()
     * @throws IOException
     */
    @Test
    public void testJdbcBatch() throws IOException {
        Connection conn = null;
        PreparedStatement ps = null;

        try {
            // 注册 JDBC 驱动
            Class.forName("com.mysql.jdbc.Driver");

            // 打开连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/gp-mybatis?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true", "root", "123456");
            ps = conn.prepareStatement(
                    "INSERT into blog values (?, ?, ?)");

            for (int i = 1000; i < 101000; i++) {
                Blog blog = new Blog();
                ps.setInt(1, i);
                ps.setString(2, String.valueOf(i));
                ps.setInt(3, 1001);
                ps.addBatch();
            }

            ps.executeBatch();
            // conn.commit();
            ps.close();
            conn.close();
        } catch (SQLException se) {
            se.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (ps != null) ps.close();
            } catch (SQLException se2) {
            }
            try {
                if (conn != null) conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            }
        }
    }

 

嵌套(关联)查询及问题 

记得用Hibernate的时候,可能一个实体类中会用到其它实体类的属性,那么一般会在那个实体类中加字段或者集合来映射,但这样做却等于污染了实体类。

那如果我们不想污染实体类,MyBatis是怎么操作的呢,举个例子,首先有两个实体类。

public class Author implements Serializable {
    Integer authorId; // 作者ID
    String authorName; // 作者名称

    public Integer getAuthorId() {
        return authorId;
    }

    public void setAuthorId(Integer authorId) {
        this.authorId = authorId;
    }

    public String getAuthorName() {
        return authorName;
    }

    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }
}

public class Blog implements Serializable{
    Integer bid; // 文章ID
    String name; // 文章标题
    Integer authorId; // 文章作者ID

    public Integer getBid() {
        return bid;
    }

    public void setBid(Integer bid) {
        this.bid = bid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAuthorId() {
        return authorId;
    }

    public void setAuthorId(Integer authorId) {
        this.authorId = authorId;
    }

    @Override
    public String toString() {
        return "Blog{" +
                "bid=" + bid +
                ", name='" + name + '\'' +
                ", authorId='" + authorId + '\'' +
                '}';
    }
}

第一种方式:嵌套结果

先建一个包装对象,然后把结果返回这个包装对象中,里面包含了作者实体类,且在resultMap中用association定义了作者实体类的全部属性,这样就等于直接把结果放到了resultMap中。

public class BlogAndAuthor implements Serializable {
    Integer bid; // 文章ID
    String name; // 文章标题
    Author author; // 作者

    public Integer getBid() {
        return bid;
    }

    public void setBid(Integer bid) {
        this.bid = bid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "BlogAndAuthor{" +
                "bid=" + bid +
                ", name='" + name + '\'' +
                ", author=" + author +
                '}';
    }
}
 <!-- 根据文章查询作者,一对一查询的结果,嵌套结果 -->
    <resultMap id="BlogWithAuthorResultMap" type="com.darker.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <!-- 联合查询,将author的属性映射到ResultMap -->
        <association property="author" javaType="com.darker.domain.Author">
            <id column="author_id" property="authorId"/>
            <result column="author_name" property="authorName"/>
        </association>
    </resultMap>

 

<!-- 根据文章查询作者,一对一,嵌套结果,无N+1问题 -->
    <select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>

第二种方式:嵌套查询

依然使用上面的包装类,但是resultMap中不直接定义全部的属性,而是用association定义一个select的查询器,也就是说当用到这个查询的时候,会去触发这个select方法。

  <!-- 嵌套查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
    <resultMap id="BlogWithAuthorQueryMap" type="com.darker.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <association property="author" javaType="com.darker.domain.Author"
                     column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
    </resultMap>

 

 <!-- 根据文章查询作者,一对一,嵌套查询,存在N+1问题,可通过开启延迟加载解决 -->
    <select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>

    <!-- 嵌套查询 -->
    <select id="selectAuthor" parameterType="int" resultType="com.darker.domain.Author">
        select author_id authorId, author_name authorName
        from author where author_id = #{authorId}
    </select>

在这种方式里面就存在一个N+1的问题,举个例子,当我们查询这个列表的时候,可能我只是需要显示博客的内容,并不需要一开始就显示作者,但是它依然会把每个selectAuthor方法走一遍,这样就等于每次查询都要查两个SQL,显然是不利于效率的,如果不调用这个属性的话,我们并不希望它多查一次SQL。 

解决办法当然也是有的,那就是开启延迟加载,上面早前说到的全局配置中settings,里面有个懒加载开关。

这里用代码调用来说明下区别:

当我关闭了延迟加载的时候,使用这个方法

    /**
     * 一对一,一篇文章对应一个作者
     * 嵌套查询,会有N+1的问题
     */
    @Test
    public void testSelectBlogWithAuthorQuery() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        BlogMapper mapper = session.getMapper(BlogMapper.class);

        BlogAndAuthor blog = mapper.selectBlogWithAuthorQuery(1);
        System.out.println("-----------:"+blog.getClass());
        // 如果开启了延迟加载,会在使用的时候才发出SQL
        // equals,clone,hashCode,toString也会触发延迟加载
        // System.out.println("-----------调用toString方法:"+blog);
        //System.out.println("-----------getAuthor:"+blog.getAuthor().toString());
        // 如果 aggressiveLazyLoading = true ,也会触发加载,否则不会
        //System.out.println("-----------getName:"+blog.getName());
    }

 

得出的答案你会发现,我并没有调用这个实体类中的作者,但它依然执行了selectAuthor这套SQL语句。

当我开启延迟加载的时候,使用这个方法 ,你会发现它只执行了一条sql。

 当我再次在方法中调用这个对象getAuthor方法的时候,它才会去执行下一条SQL,也就是说只要用到的时候才去查询。

 

当你开了全局延迟加载以后,你在某个地方又不想要延迟加载怎么办呢?

你可以在你想要的方法上加上aggressiveLazyLoading属性,默认关闭,可以用过select标签中的fetchtype来覆盖。

延迟加载的原理

显然能增强类的功能,使得每次只有getAuthor方法的时候才去调用SQL,那么原理只能是它改造了我们的getAuthor方法,能做到这个的只能是代理了。

那么通过debug你可以看到,你的对象的class其实变了。

变成了一个javassist代理对象。

Mybatis可以支持两种代理方式,一种是默认的java方法,一种是cglib方式,可以在配置种设置。 

 

MyBatis的分页问题

逻辑分页(假分页)和物理分页(真分页,使用数据库支持的语法)

逻辑分页(假分页):

Mybatis提供对象RowBounds

 /**
     * 逻辑分页
     * @throws IOException
     */
    @Test
    public void testSelectByRowBounds() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            int start = 0; // offset
            int pageSize = 5; // limit
            RowBounds rb = new RowBounds(start, pageSize);
            List<Blog> list = mapper.selectBlogList(rb); // 使用逻辑分页
            for(Blog b :list){
                System.out.println(b);
            }
        } finally {
            session.close();
        }
    }

按数据库和sql来说,必然式应该查出6条,但我传了个 RowBounds对象后,却完成了逻辑分页,那么它是怎么做到的呢,它会先把数据全部放到内存中,然后定位到偏移量offset,也就是起始值,把之前的全部去掉,然后在定位到limit也就是结尾值,然后把这部分取出来,所以,这样是非常浪费性能的。

物理分页(真分页)

最原始的方法,java层面计算分页点传入

<select id="selectBlogPage" parameterType="map" resultMap="BaseResultMap">
        select * from blog limit #{curIndex} , #{pageSize}
    </select>

插件形式,自动帮我们生成物理分页数据,pagehelper 

 

逆向工程

MaBatis-generator,自动生成映射bo,已经一些通用的curd

问题:字段表发生了变化怎么办?

思路:

(1)、继承生成的mapper文件(好维护但多了两个文件,一个继承的接口文件和一个XML)

/**
 *
 * 扩展类继承了MBG生成的接口和Statement
 *
 */
public interface BlogMapperExt extends BlogMapper {
    /**
     * 根据名称查询文章
     * @param name
     * @return
     */
    public Blog selectBlogByName(String name);
}

(2)通用Mapper(tk mybatis,mybatisplus) 

通用curd

条件构造

代码生成

翻页

批量操作

ps:

附上参考网站MyBatis中文网

MyBatis中文网 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值