Mybatis应用实践和分析

Mybatis应用实践和分析

起源

从原生JDK Jdbc开始,帮我们解决了数据库连接、SQL参数编译问题,但是仍有很多痛点:

1、重复代码

2、资源管理(创建和释放)

3、结果集处理(每个POJO都需要手动映射)

4、SQL耦合

JDK JDBC

Connection conn = null;
Statement stmt = null;

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

// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/gp-mybatis", "root", "123456");

// 执行查询
stmt = conn.createStatement();
String sql = "SELECT bid, name, author_id FROM blog where bid = 1";
ResultSet rs = stmt.executeQuery(sql);

// 获取结果集
Blog blog = new Blog();
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);
}

rs.close();
stmt.close();
conn.close();
总结

缺点:

1、重复代码

2、资源管理

3、结果集处理

4、SQL耦合


Spring JDBC

Spring提供了一个模板方法JdbcTemplate,封装了各种的execute、query、update等方法。简化了JDBC的使用,可以避免常见的异常,封装了JDBC核心流程,开发者只需要提供SQL、提取结果集即可。是线程安全的。

JdbcTemplate
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
    public JdbcTemplate(DataSource dataSource) {
        this.setDataSource(dataSource);
        this.afterPropertiesSet();
    }
  ...
}

public interface JdbcOperations {
    @Nullable
    <T> T execute(ConnectionCallback<T> var1) throws DataAccessException;

    @Nullable
    <T> T execute(StatementCallback<T> var1) throws DataAccessException;

    void execute(String var1) throws DataAccessException;

    <T> List<T> query(String var1, RowMapper<T> var2) throws DataAccessException;

    @Nullable
    <T> T queryForObject(String var1, RowMapper<T> var2) throws DataAccessException;
    
    ...
}

另外也提供了结果集转换的接口,开发者只需要实现接口方法mapRow,把结果集转换为对象。

RowMapper
@FunctionalInterface
public interface RowMapper<T> {
    @Nullable
    T mapRow(ResultSet var1, int var2) throws SQLException;
}
例子
public class EmployeeRowMapper implements RowMapper {
    @Override
    public Object mapRow(ResultSet resultSet, int i) throws SQLException {
        Employee employee = new Employee();
        employee.setEmpId(resultSet.getInt("emp_id"));
        employee.setEmpName(resultSet.getString("emp_name"));
        employee.setGender(resultSet.getString("gender"));
        employee.setEmail(resultSet.getString("email"));
        return employee;
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class EmployeeRowMapperTest {

    @Autowired
    JdbcTemplate jdbcTemplate;

    List<Employee> list;

    @Test
    public void EmployeeTest() {
        list = jdbcTemplate.query("select * from tbl_emp", new EmployeeRowMapper());
        System.out.println(list);
    }
}
总结

优点:

1、无需关心资源管理

2、

缺点:

1、SQL耦合

2、当项目的表慢慢越多的时候,每张表转为POJO都需要定义一个RowMapper,导致类文件数量膨胀

思考

Spring JDBC 对 JDBC 做了轻量级封装的框架,帮我们解决的问题:
1、对数据的增删改查方法进行了封装

2、不用手动创建关闭资源

3、可以帮助我们映射结果集

不足:

1、SQL语句写死在代码里

2、参数只能按固定的顺序传入(数组),是通过占位符替换的。不能传入对象和Map

3、在方法里可以把结果集映射成实体类,但是不能直接把实体类映射成数据库的记录

4、查询没有缓存的功能,性能还不够好

由此引出ORM框架

到了Spring时代,整合了JDBC,对JDBC做了进一步封装。核心类 JdbcTemplate 处理了资源的管理以及SQL的执行封装,RowMapper 提供了结果集自定义映射的方法。帮助我们解决了很大的问题,但仍存在一些不足:

1、SQL语句写死在代码里

2、参数只能按固定的顺序传入(数组),是通过占位符替换的。不能传入对象和Map

3、在方法里可以把结果集映射成实体类,但是不能直接把实体类映射成数据库的记录

4、查询没有缓存的功能,性能还不够好

ORM框架

Object-对象 Mapping-映射 Relational-关系

Mybatis

“半自动化”的ORM框架,是对于Hibernate的全自动化来说的。它的封装程序没有Hibernate那么高,不会自动生成全部的SQL语句,主要是解决SQL和对象的映射问题

前身

前身是 ibatis,2001年开始开发,是 “internet” 和 “abatis” 两个单词的组合。04年捐赠给Apache,2010年更名为Mybatis

https://mybatis.org/mybatis-3/zh/

编程式开发
引包
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.4</version>
</dependency>
全局配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <properties resource="db.properties"></properties>
    <settings>
        <!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!-- 控制全局缓存(二级缓存),默认 true-->
        <setting name="cacheEnabled" value="true"/>

        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
        <!--<setting name="proxyFactory" value="CGLIB" />-->
        <!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
        <!--
                <setting name="localCacheScope" value="STATEMENT"/>
        -->
        <setting name="localCacheScope" value="SESSION"/>
    </settings>

    <typeAliases>
        <typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
    </typeAliases>

<!--    <typeHandlers>
        <typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
    </typeHandlers>-->

    <!-- 对象工厂 -->
<!--    <objectFactory type="com.gupaoedu.objectfactory.GPObjectFactory">
        <property name="gupao" value="666"/>
    </objectFactory>-->

<!--    <plugins>
        <plugin interceptor="com.gupaoedu.interceptor.SQLInterceptor">
            <property name="gupao" value="betterme" />
        </plugin>
        <plugin interceptor="com.gupaoedu.interceptor.MyPageInterceptor">
        </plugin>
    </plugins>-->

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="BlogMapper.xml"/>
        <mapper resource="BlogMapperExt.xml"/>
    </mappers>

</configuration>
Mapper.xml

测试
    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void prepare() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    /**
     * 使用 MyBatis API方式
     * @throws IOException
     */
    @Test
    public void testStatement() throws IOException {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            Blog blog = (Blog) session.selectOne("com.gupaoedu.mapper.BlogMapper.selectBlogById", 1);
            System.out.println(blog);
        } finally {
            session.close();
        }
    }


    /**
     * 通过 SqlSession.getMapper(XXXMapper.class)  接口方式
     * @throws IOException
     */
    @Test
    public void testSelect() throws IOException {
        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();
        }
    }
核心特性
  • 使用连接池对连接进行管理
  • SQL 和代码分离,集中管理
  • 重复SQL提取
  • 参数映射和动态SQL
  • 结果集映射
  • 缓存管理
  • 插件机制
核心对象的生命周期
SqlSessionFactoryBuilder

方法局部

它是用来构建SqlsessionFactory的,而SqlsessionFactory只需要一个,所以构建完成后它的使命就完成了。
所以它的生命周期只存在于方法的局部。
主要解析全局配置和Mapper文件。
SqlSessionFactory

应用级别

它是用来创建Sqlsesion会话的,每次应用访问数据库,都需要创建一个会话。SqlSessionFactory是存在于应用的整个生命周期。创建Sqlsession只需要一个实例来完成,否则就会产生很多的混乱和资源浪费。需要采用单例模式。
SqlSession

一次请求或一次操作

SqlSession是一个会话,它不是线程安全的,不能在线程间共享。所以我们在请求开始的时候创建一个SqlSession对象,在请求结束或方法执行完毕时要及时关闭,
Mapper

方法

实际是一个代理对象,是从Sqlsession中获取的。它的作用是发送SQL来操作数据库的数据,它应该在一个Sqlsession事务方法之内。
核心配置解读
<configuration>

    <properties resource="db.properties"></properties>
    <settings>
        <!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!-- 控制全局缓存(二级缓存),默认 true-->
        <setting name="cacheEnabled" value="true"/>

        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>
        
        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="true"/>
        
        <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
        <!--<setting name="proxyFactory" value="CGLIB" />-->
        
        <!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
        <!--
                <setting name="localCacheScope" value="STATEMENT"/>
        -->
        <setting name="localCacheScope" value="SESSION"/>
    </settings>

    <typeAliases>
        <typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
    </typeAliases>

<!--    <typeHandlers>
        <typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
    </typeHandlers>-->

    <!-- 对象工厂 -->
<!--    <objectFactory type="com.gupaoedu.objectfactory.GPObjectFactory">
        <property name="gupao" value="666"/>
    </objectFactory>-->

<!--    <plugins>
        <plugin interceptor="com.gupaoedu.interceptor.SQLInterceptor">
            <property name="gupao" value="betterme" />
        </plugin>
        <plugin interceptor="com.gupaoedu.interceptor.MyPageInterceptor">
        </plugin>
    </plugins>-->

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="BlogMapper.xml"/>
    </mappers>

</configuration>
configuration
properties

配置参数信息,常见的如数据库连接信息。为了避免写死一些参数在xml文件中,可以放到properties,需要的时候引入进来, 在xml配置文件中可以用${}引用

settings

settings中是Mybatis的一些核心配置。比如 一级缓存、二级缓存、延迟加载等

typeAliases

可以配置返回类型的别名,和Linux里的alias一样,主要是简化全路径的拼写。

<typeAliases>
	<typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
</typeAliases>
<select id="selectBlogByBean"  parameterType="blog" resultType="blog" >
	select bid, name, author_id authorId from blog where name = '${name}'
</select>
typeHandlers

解决Java类型和数据库的JDBC类型不是一一对应的(比如String和varchar、char、text),所以把Java对象转为数据库的值、数据库的值转为Java对象,需要经过一层转换。

在Mybatis已经内置了很多TypeHandler(type包下),全部都注册在TypeHandlerRegistry

objectFactory

当把数据库返回的结果集转为实体类的时候,需要创建对象的实例,通过设置返回的class类型反射创建。

plugins

这是Mybatis一个很强大的机制,和其它框架一样,预留了插件的接口,让Mybatis更容易扩展。

可拦截的4个对象类型:

1、Executor(query、update、commit、rollback、close)

2、ParameterHandler(getParameterObject、setParameters)

3、ResultSetHandler(handleResultSets、handleOutputParameters)

4、StatementHandler(prepare、parameterize、batch、update、query)

environments
environment

transactionManager

dataSource

Mybatis自带了2种数据源,POOLED和UNPOOLED。也可以配置成其它数据库,比如C3P0\HiKari等。

为什么要是用连接池?

除了连接池,像比如线程池、内存池、对象池,这些池化技术达到的目的基本一样:

1、减少连接的创建和关闭(复用连接)

如果没有池化技术,每次连接都是一个物理连接,创建和关闭都会消耗应用和服务器的性能;

如果使用连接池,在应用关闭连接时,物理连接并没有真正关闭连接,只是回到了连接池中。

那么从这个角度考虑,一般连接池都会有初始化连接数、最大连接数、回收时间等等这些配置参数。提供【提前创建、资源重用、数量控制、超时管理】等等这些功能

mapper

配置的是映射器,也就是Mapper.xml 路径。这是配置目的是让Mybatis在启动的时候扫描这些映射器,创建映射关系。

有4种指定Mapper文件的方式:

1、使用相对于类路径的资源引用(resource)

<mappers>
	<mapper resource="BlogMapper.xml"/>
</mappers>

2、使用完全限定资源定位符(绝对路径,URL)

<mappers>
	<mapper resource="file:///D//work//BlogMapper.xml"/>
</mappers>

3、使用映射器接口类的完全限定类名

<mappers>
	<mapper resource="com.gupaoedu.mapper.BlogMapper"/>
</mappers>

4、将包内的映射器接口实现全部注册为映射器(最常见)

<mappers>
	<mapper resource="com.gupaoedu.mapper/>
</mappers>
动态SQL
if
choose(when otherwise)
trim(set)
foreach
where
批量操作
循环单次发送

如果是几万条的执行,意味着需要创建几万次会话连接。即使在同一个会话中,也有重复编译和执行发送SQL的开销。

批量发送
foreach标签

批量新增

insert into blog (bid, name, author_id)
values
<foreach collection="list" item="blog" index="index" separator=",">
  (#{blog.bid}, #{blog.name}, #{blog.authorId})
</foreach>

批量更新

update blog 
set
    name = case bid 
    when ? then ?
    when ? then ?
    end,
    author_id = case bid
    when ? then ?
    when ? then ?
    end
where bid in (?, ?)
update blog 
set
    name = case bid 
    <foreach  collection="list" item="blog" index="index" separator=" " >
    when #{blog.bid} then #{blog.name}
    </foreach>
    end
where bid in
<foreach collection="list" item="blog" index="index" separator="," open="(" close=")">
  #{blog.bid}
</foreach>

优点:可以减少和数据库的交互次数,并且避免了开启和结束事务的时间消耗

缺点:当数据量特别大的时候,拼接出来的SQL语句过大。MYSQL的服务端对接收的数据包有大小限制,

max_allowed_packet 默认是4M,可以手动修改

Batch Executor
  • SimpleExecutor

    每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

  • ReuseExecutor

    执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建。用完后,不关闭Statement对象,而是放置于Map中。简而言之,就是重复使用Statement对象

  • BatchExecutor

​ 执行update(JDBC批处理不支持select),将所有sql都添加到批量处理中(addBatch),等统一处理。它缓存了多个 Statement对象,每个Statement对象都是addBatch完毕后,等逐一执行executeBatch批处理。

和JDBC批处理相同。

​ executeBatch是一批语句访问一次数据库(具体发送多少·和服务端设置的max_allowed_packet 有关)。

​ BatchExecutor底层是对JDBC ps.addBatch() 和 ps.executeBatch 的封装

关联查询与延迟加载

映射结果有2个标签,resultType 和 resultMap

  • resultType
是select标签的一个返回属性,适用于JDK类型(Integer、String等)和实体类,这种情况结果集的列和实体类  的属性可以直接映射。如果返回的字段无法映射,可以resultMap来建立映射关系。

而对于关联查询,多对一、1对多等情况,就需要用到resultMap中的 associationcollection

association 用于一对一、多对一,而collection用于一对多的关系

嵌套查询

会带来N + 1问题,主查询SQL是1,SQL结果是多条,又会执行N条SQL语句

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

如何解决呢?

使用延迟加载,只要当对象get该属性时,才会二次查询。(原理是动态代理实现)

<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
<setting name="lazyLoadingEnabled" value="true"/>

<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
翻页
逻辑翻页

Mybatis自带的逻辑翻页对象 RowBounds,主要有2个属性:offset、limit(从第几条开始,查询多少条)。

工作原理就是对 ResultSet 的内存处理。

List<Blog> selectBlogList(RowBounds rowBounds);
物理翻页

物理翻页才是真正的翻页,通过数据库支持的语法来翻页查询。

PageHelper
常见问题

1、是用注解还是用xml配置?

注解的缺点就是SQL无法集中管理,复杂的SQL比较难配置。所以建议在业务复杂的项目中使用XML配置的形式,简单项目中使用注解和XML混用的形式。

2、Mapper接口注入 Invalid bound statement (not found) 问题

Mapper接口无法注入,或者mapper statementid 跟 Mapper接口无法绑定的情况,基于绑定的要求规范,可以从这些地方检查:

  • 扫描配置,xml文件和Mapper接口没有扫描到
  • namespace值是否和接口一致
  • 检查对应的sql语句ID是否存在(接口上的方法)

3、怎么获取插入的最新自动生成的ID?

4、什么时候用#{},什么时候用${}

5、XML中怎么使用特殊符号,比如小于 &

6、如何实现模糊查询 LIKE

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

子津

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值