Mybatis全方位剖析【一】——为什么要使用Mybatis?

Mybatis全方位剖析【一】——为什么要使用Mybatis?

一、什么是Mybatis

Mybatis 的前身是ibatis,2001年开始开发,是 “internet” 和 “abatis” 两个单词的组合。04年捐赠给Apache。2010年更名为Mybatis。Mybatis是一个"半自动化"的 ORM 框架,其实就是封装JDBC用于增删改查的框架。相对于Hibernate的全自动化来说。它的封装程度没有Hibernate那么高,在Mybatis里面,SQL和代码是分离的,它也不会自动生成全部的SQL语句,主要解决了SQL和对象的映射问题。

  • ORM(Object Relational Mapping): 对象关系映射,简单说就是Java中对象和关系型数据库中一条记录的映射关系

简单了解Mybatis后,我将从JDBC操作数据库开始说起,了解JDBC的不足,及其后续一些工具类的不足,最后延伸出我们的"半自动"的ORM框架Mybatis。

二、数据准备

在开始之前,先创建几张demo演示表,分别为用户、博客、评论表:

CREATE TABLE `blog` (
  `bid` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `author_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`bid`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `author` (
  `author_id` int(16) NOT NULL AUTO_INCREMENT,
  `author_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`author_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1002 DEFAULT CHARSET=utf8;

CREATE TABLE `comment` (
  `comment_id` int(16) NOT NULL AUTO_INCREMENT,
  `content` varchar(255) DEFAULT NULL,
  `bid` int(16) DEFAULT NULL,
  PRIMARY KEY (`comment_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

INSERT INTO `blog` (`bid`, `name`, `author_id`) VALUES (1, 'MyBatis分析一', 1001);
INSERT INTO `blog` (`bid`, `name`, `author_id`) VALUES (2, 'MyBatis分析二', 1002);
INSERT INTO `author` (`author_id`, `author_name`) VALUES (1001, '扶我起来');
INSERT INTO `comment` (`comment_id`, `content`, `bid`) VALUES (1, '扶我起来,还能码', 1);
INSERT INTO `comment` (`comment_id`, `content`, `bid`) VALUES (2, '扶我起来,继续码', 1);

三、传统JDBC方式操作数据库

下面我用JDBC的方式来查询blog表中bid为1的数据:

    @Test
    public void jdbcDemo(){
        Connection conn = null;
        Statement stmt = null;
        Blog blog = new Blog();
        try {
            // 注册JDBC驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 打开连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "4124");
            // 执行查询
            stmt = conn.createStatement();
            String sql = "SELECT bid, name, author_id FROM blog where bid = 1";
            ResultSet rs = stmt.executeQuery(sql);
            // 获取结果集
            while (rs.next()) {
                Integer bid = rs.getInt("bid");
                String name = rs.getString("name");
                Integer authorId = rs.getInt("author_id");
                blog.setAuthorId(authorId);
                blog.setBid(bid);
                blog.setName(name);
            }
            System.out.println(blog);
            rs.close();
            stmt.close();
            conn.close();
        } catch (Exception se) {
            se.printStackTrace();
        } finally {
            try {
                if (stmt != null) {
                	stmt.close();
               	}
            } catch (SQLException e2) {
                e2.printStackTrace();
            }
            try {
                if (conn != null) {
                	conn.close();
                }
            } catch (SQLException se) {
                se.printStackTrace();
            }
        }
    }

控制台输出:
在这里插入图片描述
从以上代码我们可以看出JDBC方式去操作数据库大致分为四个步骤:

  • 通过Class.forName(“com.mysql.jdbc.Driver”);注册驱动;
  • 通过DriverManager获取一个Connection;
  • 通过Connection获取一个Statement对象;
  • 通过Statement 的execute()方法执行SQL拿到ResultSet 结果集;

最后通过ResultSet获取数据,给Blog对象的属性赋值,关闭数据库相关资源。
如果我们项目中使用JDBC的方式来操作数据库,一旦项目的业务比较复杂,数据库表很多的话,那么这样的代码将会重复出现很多次。在每一段这样的代码里面都需要自己去管理数据库的连接资源,如果一旦忘记关闭连接资源,那么就很可能会造成数据库服务连接耗尽的问题。对于结果集的处理,当我们需要将ResultSet转换成POJO的时候,就只有根据字段属性的类型一个个去set,还有一个就是处理业务逻辑和处理数据的代码是耦合在一起的。如果业务流程非常复杂,跟数据库的交互次数过多,耦合在代码中的SQL语句将会非常之多。简单总结一下,使用JDBC将会有以下不足:

  • 重复代码
  • 资源管理
  • 结果集处理
  • SQL耦合

到这里可见JDBC的很多不足,那么怎么解决这么些问题?在2003年Apache发布了 Commons DbUtils工具类,用于简化对数据库的操作。

四、Apache DbUtils

Apache DbUtils中,通过传入数据源的方式解决了数据库链接资源管理的问题,通过QueryRunner这个类对数据库增删改查的方法的封装,解决了重复代码的问题,通过ResultSetHandler这个类解决了结果集转换的问题,接下来我们具体来看一下它到底是怎么处理的:

1.数据源初始化
HikariDataSource 数据源的初始化只需读取配置文件就可以了

        private static final String PROPERTY_PATH = "/hikari.properties";
        
        HikariConfig config = new HikariConfig(PROPERTY_PATH);
        HikariDataSource dataSource = new HikariDataSource(config);

2.QueryRunner和ResultSetHandler的使用

	//获取QueryRunner 
 	QueryRunner queryRunner = new QueryRunner(dataSource);
 	
    /**
     * @Description: 返回单个对象,通过new BeanHandler<>(Class<?> clazz)来设置封装
     * @Date 2021-11-25 17:18
     */
    public static void selectBlog(Integer bid) throws SQLException {
        String sql = "select * from blog where bid = ? ";
        Object[] params = new Object[]{bid};
        BlogDto blogDto = queryRunner.query(sql, new BeanHandler<>(BlogDto.class), params);
        System.out.println(blogDto);
    }

    /**
     * @Description: 返回列表,通过new BeanListHandler<>(Class<?> clazz)来设置List的泛型
     * @Date 2021-11-25 17:18
     */
    public static void selectList() throws SQLException {
        String sql = "select * from blog";
        List<BlogDto> list = queryRunner.query(sql, new BeanListHandler<>(BlogDto.class));
        //list.forEach(System.out::println);
    }

ResultSetHandler 是如何做的呢?我们看源码可以知道,DbUtils在这里使用反射,获取到Blog对象的所有属性,通过for循环将其一个个使用setter方法进行值的设置
在这里插入图片描述

3.测试

    public static void main(String[] args) throws Exception {
        HikariUtil.init();
        BlogDao.selectBlog(1);
        System.out.println("========================");
        BlogDao.selectList();
    }

4.控制台打印输出
在这里插入图片描述
这里看到属性authorId为null,是因为数据库字段为author_id,和Blog对象属性不匹配,这种自动映射,要求数据库的字段跟对象的属性名称完全一致,才可以实现自动映射。源码中会会去忽略大小写去匹配,但是没有处理驼峰:
在这里插入图片描述
以上就是DbUtils的一个基本使用了,我们接着看看Spring JDBC是如何对上述问题进行处理的。

五、Spring JDBC

Spring JDBC中,通过传入数据源的方式解决了数据库链接资源管理的问题,JdbcTemplate 模板方法类封装了JDBC的核心流程,只需要提供SQL,就可以提取结果集了,解决了重复代码的问题;Spring JDBC通过RowMapper接口解决了结果集转换的问题,接下来我们具体来看一下Spring JDBC 到底是怎么处理的:
1.数据源及JdbcTemplate的初始化
jdbc.propertis配置文件配置了数据源的连接信息
通过注册Bean的方式初始化DataSourceJdbcTemplate

    <!--装载外部配置文件jdbc.properties-->
    <bean id="jdbc" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations" value="classpath*:jdbc.properties"/>
    </bean>

    <!-- Spring的连接池 -->
   <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
    </bean>
   <!--注册jdbcTemplate-->
   <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" >
        <property name="dataSource" ref="dataSource"/>
   </bean>

2.RowMapper结果集转换
Jdbc提供了RowMapper接口,可以将结果集转换成Java对象,可以实现RowMapper接口,重写mapRow()方法。在mapRow()方法中完成对结果集的转换

/**
 * @Description: Blog实体类
 * @Author zdp
 * @Date 2021-12-07 11:13
 */
@Data
public class Blog implements Serializable {

    private Integer bid;
    private String name;
    private Integer authorId;

}
/**
 * @Description: RowMapper结果集转换
 * @Author zdp
 * @Date 2021-12-07 11:13
 */
public class BlogMapper implements RowMapper<Blog> {

    public Blog mapRow(ResultSet resultSet, int i) throws SQLException {
        Blog blog = new Blog();
        blog.setAuthorId(resultSet.getInt("author_id"));
        blog.setBid(resultSet.getInt("bid"));
        blog.setName(resultSet.getString("name"));
        return blog;
    }

}

3.测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TemplateTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void queryBlogTest() {
        List<Blog> blogList = jdbcTemplate.query(" select * from blog", new BlogMapper());
        System.out.println(blogList);
    }
}

4.控制台打印输出
在这里插入图片描述
通过以上方式,我们对于结果集的处理只需要写一次代码,然后再每一个需要映射的地方传入RowMapper就可以了,减少了重复代码。但是以上方式还有一个缺点就是,对于每一个实体类对象,都需要定义一个Mapper,需要编写每个字段映射的getString(),getInt()类型这样的代码,增加了类的数量。所以是否可以考虑让数据库一行数据的字段,跟实体类的属性自动映射起来,并且数据库的JDBC类型要和Java对象的类型要匹配起来。接下来我们对上面优化一下:

/**
 * @Description: BaseRowMapper<T>,通过反射的方式自动获取所有属性,把表字段全部赋值到属性。
 * @Author zdp
 * @Date 2021-12-07 11:56
 */
public class BaseRowMapper<T> implements RowMapper<T> {

    private Class<?> targetClazz;
    private HashMap<String, Field> fieldMap;

    public BaseRowMapper(Class<?> targetClazz) {
        this.targetClazz = targetClazz;
        fieldMap = new HashMap<String, Field>();
        Field[] fields = targetClazz.getDeclaredFields();
        for (Field field : fields) {
            fieldMap.put(field.getName(), field);
        }
    }

    public T mapRow(ResultSet rs, int arg1) throws SQLException {
        T obj = null;
        try {
            obj = (T) targetClazz.newInstance();
            final ResultSetMetaData metaData = rs.getMetaData();
            int columnLength = metaData.getColumnCount();
            String columnName;
            for (int i = 1; i <= columnLength; i++) {
                columnName = metaData.getColumnName(i);
                Class<?> fieldClazz = fieldMap.get(camel(columnName)).getType();
                Field field = fieldMap.get(camel(columnName));
                field.setAccessible(true);
                if (fieldClazz == int.class || fieldClazz == Integer.class) {
                    field.set(obj, rs.getInt(columnName));
                } else if (fieldClazz == boolean.class || fieldClazz == Boolean.class) {
                    field.set(obj, rs.getBoolean(columnName));
                } else if (fieldClazz == String.class) {
                    field.set(obj, rs.getString(columnName));
                } else if (fieldClazz == float.class) {
                    field.set(obj, rs.getFloat(columnName));
                } else if (fieldClazz == double.class || fieldClazz == Double.class) {
                    field.set(obj, rs.getDouble(columnName));
                } else if (fieldClazz == BigDecimal.class) {
                    field.set(obj, rs.getBigDecimal(columnName));
                } else if (fieldClazz == short.class || fieldClazz == Short.class) {
                    field.set(obj, rs.getShort(columnName));
                } else if (fieldClazz == Date.class) {
                    field.set(obj, rs.getDate(columnName));
                } else if (fieldClazz == Timestamp.class) {
                    field.set(obj, rs.getTimestamp(columnName));
                } else if (fieldClazz == Long.class || fieldClazz == long.class) {
                    field.set(obj, rs.getLong(columnName));
                }
                field.setAccessible(false);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return obj;
    }

    /**
     * 下划线转驼峰
     */
    public static String camel(String str) {
        Pattern pattern = Pattern.compile("_(\\w)");
        Matcher matcher = pattern.matcher(str);
        StringBuffer sb = new StringBuffer(str);
        if(matcher.find()) {
            sb = new StringBuffer();
            matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
            matcher.appendTail(sb);
        }else {
            return sb.toString();
        }
        return camel(sb.toString());
    }
}

修改测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TemplateTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void queryBlogListTest() {
        List<Blog> blogList = jdbcTemplate.query(" select * from blog", new BaseRowMapper<Blog>(Blog.class));
        System.out.println(blogList);
    }
}

控制台输出
在这里插入图片描述
这样优化后,就只需要传入我们需要转换的类型就可以了,不再需要我们再单独创建RowMapper了,减少了类的数量,节省了工作量。
以上就是DbUtils和Spring JDBC的基本使用,从上面可以看出来,DbUtils和Spring JDBC 都解决了一些问题:

  1. 无论是QueryRunner还是JdbcTemplate,都可以传入一个数据源进行初始化,也就是资源管理这一部分的事情,可以交给专门的数据源组件去做,不用我们手动创建和关闭;
  2. 对操作数据的增删改查方法进行了封装;
  3. 可以帮助我们映射结果集,无论是映射成List、Map还是实体类;

但是还是存在一些缺点,比如:

  1. SQL语句都是写死再代码里面的,依旧存在硬编码的问题;
  2. 参数只能按固定位置的顺序传入(数组),它是通过占位符去替换的,不能自动映射;
  3. 在方法里面,可以把及国际映射成实体类,但是不能直接把实体类映射成数据库的记录;
  4. 查询没有缓存的功能;

六、Mybatis

ORM框架Mybatis就解决了这几个问题。简单总结下MyBatis主要解决了什么问题:

  1. 使用连接池对连接进行管理
  2. SQL和代码分离,集中管理
  3. 结果集映射
  4. 参数映射和动态SQL
  5. 重复SQL的提取
  6. 缓存管理
  7. 插件机制

当然,MyBatis和DbUtils、Spring JDBC一样,都是对JDBC的封装。底层都是Statement和ResultSet这些对象。下一节我们接着剖析MyBatis,看看MyBatis是如何来操作的!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值