从零开始认识并操纵Mybatis

目录

@(认识并操纵Mybatis)

声明:从零开始,并不代表你对java Mybatis一点都不懂的程度哈,本实例只是过一个概貌,详细内容会分多篇文章拆解

业务介绍

用户模块的管理,用户表的维护:

  1. 添加用户
  2. 修改用户信息
  3. 删除用户
  4. 查询用户
    1. id查询单个
    2. 用户名模糊查询
    3. 查询所有
    4. 查询总数
    5. 根据ids查询
    6. 多条件查询

用户和订单关联管理

  1. 根据订单查询对应用户信息
  2. 根据用户查询所有订单信息

版本声明

只使用Mybatis,不集成其他框架。Mybatis版本为^3.4

开发工具使用eclipse 使用的是纯 java Project

整个流程的详细项目代码可以踩我的github结合观光:https://github.com/Autom-liu/MybatisLearn 每一个步骤都分得很清晰哦,可以clone下来再研究

认识Mybatis

是什么

MyBatis是一个优秀的持久层框架,不算完整的ORM框架

历史

  • MyBatis 本是apache的一个开源项目iBatis,
  • 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。
  • 2013年11月迁移到Github。

传统JDBC存在的问题

  • 数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
    • Mybatis 在配置文件中配置了数据连接池,使用连接池管理数据库链接。
  • Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
    • Mybatis 将Sql语句配置在配置文件中与java代码分离。
  • 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
    • Mybatis自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。
  • 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
    • Mybatis自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。

与完整的ORM框架Hibernate的区别

  • Mybatis不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。mybatis可以通过XML或注解方式灵活配置要运行的sql语句,并将java对象和sql语句映射生成最终执行的sql,最后将sql执行的结果再映射生成java对象。
  • Mybatis学习门槛低,简单易学,程序员直接编写原生态sql,可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁,一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。
  • Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件(例如需求固定的定制化软件)如果用hibernate开发可以节省很多代码,提高效率。但是Hibernate的学习门槛高,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡,以及怎样用好Hibernate需要具有很强的经验和能力才行。
  • 总之,按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。

简单来说,Mybatis为什么流行,Hibernate为什么落幕,终归在于移动互联网的世界趋势,迭代更新快,要求变化杂而多。不可能再像以往那样,形成完整稳定的需求再开发的阶段了….越贴近底层原生越有价值,越是封装包装限制也越大

Mybatis架构

image.png

入门程序操作步骤

下载和导包

下载链接:https://github.com/mybatis/mybatis-3/releases

目录结构:

image.png

导包:

  • Mybatis核心包
  • Mybatis依赖包
  • 数据库驱动包 注:这里使用的是老版本的5.1的包,新版本有很大改动!

准备数据库和Java Bean

两张表: 用户表 + 订单表 基本创建即可,无其他关联

image.png

image.png

准备javaBean 就不多说了,要注意的一点就是,javaBean最好实现
Serializable序列化接口持久化,还有一点就是命名规范吧,不同人说法不同,有些是直接干脆和数据库字段名一样,而这里不那么一致,使用编程语言中统一的驼峰命名法,而不是数据库中的短划线分割。

要相信的是,任何一个框架总会帮我们解决这个映射问题的!

配置Mybatis

在官方给出的文档当中给出了我们这样的配置:

官方的文件名是: mybatis-config.xml 当然这并不是定死的。

<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>

这个是主配置文件,也就是架构当中的最顶层,基本上参照这个配置copy,稍作修改即可,就不再导入约束啥的了。

比如在本次配置里,创建的是src/sqlMap.xml,文件名不是固定的,后边程序会指定

还有一点就是:不用太在意每个配置项的含义,大概看得懂就行,因为后期往往需要整合spring,这些配置全部不用了,作为入门程序而已,给大家观光一下

<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="****"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="sqlMap/User.xml"/>
    </mappers>
</configuration>

编写sql映射文件

这里可以看到Mapper标签,是引入其他映射文件,这个文件就是架构当中的第二层,多个mapper的组成,每个mapper都是若干个sql语句

到这里就需要考虑实现业务的sql语句应该如何书写了,这里先只贴出增和查的业务,剩下都以举例完成

sqlMap/User.xml

<mapper namespace="user">
    <select id="findUser" resultType="edu.scnu.bean.User" parameterType="Integer">
        select * from mb_user where id = #{id}
    </select>
    <select id="getByUsername" resultType="edu.scnu.bean.User" parameterType="String">
        select * from mb_user where username like "%"#{username}"%"
    </select>
    <insert id="save" parameterType="edu.scnu.bean.User">
        insert into mb_user (username,birthday,sex,address) values (#{username}, #{birthday}, #{sex}, #{address})
    </insert>
</mapper>

namespace 是命名空间,这个真正的妙用后面体现出来,这里暂时只需要知道用于区分同名作用域即可

id 可以认为是该条sql语句的标识符

parameterType 传递进来的参数类型,可以看到基本数据类型可以省略,而其他的就要完整类名了

resultType 返回值类型,同上一样

select 标签用于书写select查询语句

insert 标签用于书写insert插入语句 update delete一样

尝试了一下 select 书写insert好像也可以,但不推荐

关于模糊查询的补充

在Mybatis中使用 #{} 作为占位符,占位符的作用在于会在标识符取得结果后前后加单引号。

如果直接使用:

select * from mb_user where username like “%#{username}%”

就不对了,因为假如username=admin 结果变为:

select * from mb_user where username like “%’admin’%”

就语法错误或没有结果

可以使用 ${} 这种字符串拼接的方式

select * from mb_user where username like “%${username}%”

不过这样只是没有了占位符参数的效果了而已。

书写Dao类

接下来就到来书写真正的Dao类了,这里还是按照规范实现接口

public interface UserDao {
    void save(User user);
    void update(User user);
    void delete(Integer id);
    User find(Integer id);
//  List<User> getAll();
//  Integer getCount();
    List<User> getByUsername(String username);
//  List<User> getByCond(User user);
}

看下实现类:

public class UserDaoImpl implements UserDao {

    private SqlSessionFactory sqlSessionFactory;

    // 没有spring帮助下,就需要外部手动注入!
    public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public void save(User user) {
        SqlSession session = sqlSessionFactory.openSession();
        session.insert("user.save", user);
        session.commit();
    }

    @Override
    public User find(Integer id) {
        SqlSession session = sqlSessionFactory.openSession();
        return session.selectOne("user.findUser", id);
    }

    @Override
    public List<User> getByUsername(String username) {
        SqlSession session = sqlSessionFactory.openSession();
        return session.selectList("user.getByUsername", username);
    }

}

测试类

最后即可编写测试了

public class MybatisTest {
    
    private SqlSessionFactory sqlSessionFactory;
    private UserDao userDao;
    
    @Before
    public void before() throws Exception{
        String resource = "sqlMap.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        userDao = new UserDaoImpl(sqlSessionFactory);
    }
    
    @Test
    public void testFind() {
        userDao.find(1);
    }
    
    @Test
    public void testGetByUsername() {
        userDao.getByUsername("王");
    }
    
    @Test
    public void testSave() {
        userDao.save(new User("未知用户", "2", new Date(), "中国"));
    }
}

返回id 的save

现在对实现增加用户新增需求,就是在新增用户的同时根据该用户ID查询该订单信息

这时候我们首先要获取新增用户后的id,但是新增用户是数据库给我们返回的是受影响条数,如何快速获得最新插入数据的id呢

sql语法提供了 select LAST_INSERT_ID()供我们获取最新插入数据的ID,我们可以将该sql嵌入到sql配置文件中去

这里简单讲解配置语法:

  • selectKey 标签实现主键返回
  • keyColumn:主键对应的表中的哪一列
  • keyProperty:主键对应的pojo(Bean)中的哪一个属性
  • order:设置在执行insert语句前执行查询id的sql,还是在执行insert语句之后执行查询id的sql

使用主键自增策略的id:

<!-- 生成主键的顺序Mysql是之后,oracle是之前 -->
<selectKey keyProperty="id" resultType="Integer" order="AFTER">
    select LAST_INSERT_ID()
</selectKey>

使用uuid的String

<selectKey keyColumn="id" keyProperty="id" order="BEFORE" resultType="string">
    select LAST_INSERT_ID()
</selectKey>

这里可以修改上述插入SQL语法配置:

<insert id="save" parameterType="edu.scnu.bean.User">
    <!-- 生成主键的顺序Mysql是之后,oracle是之前 -->
    <selectKey keyProperty="id" resultType="Integer" order="AFTER">
        select LAST_INSERT_ID()
    </selectKey>
    insert into mb_user (username,birthday,sex,address) values (#{username}, #{birthday}, #{sex}, #{address})
</insert>

原始Dao开发方式存在的问题

  • Dao方法体存在重复代码:通过SqlSessionFactory创建SqlSession,调用SqlSession的数据库操作方法
  • 调用sqlSession的数据库操作方法需要指定statement的id,这里存在硬编码,不得于开发维护。

上述问题主要关键还是在于同类型的枯燥性重复性代码,只有少数参数传递部分要变动,其它基本都是同样的套路,因此可不可以有个办法一些重复性工作呢?Mybatis贴心为我们服务了这一点,它省去的不仅仅是一两条语句,它把整个实现类都为我们省去了。来看看它是怎么工作的吧!

Mapper 动态代理方式

Mapper接口开发方法只需要程序员编写Mapper接口(相当于Dao接口),由Mybatis框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。

Mapper接口开发需要遵循以下规范:
1、 Mapper.xml文件中的namespace与mapper接口的类路径相同。
2、 Mapper接口方法名和Mapper.xml中定义的每个statement的id相同
3、 Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同
4、 Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同

也就是说,我们要完成的只是接口,实现类框架帮我们搞定了…

那么首先来修改优化一下配置文件

<!-- 使用动态代理开发DAO,1. namespace必须和Mapper接口类路径一致 -->
<mapper namespace="edu.scnu.dao.UserDao">
    <!-- 根据用户id查询用户 -->
    <!-- 2. id必须和Mapper接口方法名一致 -->
    <!-- 3. parameterType必须和接口方法参数类型一致 -->
    <!-- 4. resultType必须和接口方法返回值类型一致 -->
    <select id="find" resultType="edu.scnu.bean.User" parameterType="Integer">
        select * from mb_user where id = #{id}
    </select>
    <select id="getByUsername" resultType="edu.scnu.bean.User" parameterType="String">
        select * from mb_user where username like "%"#{username}"%"
    </select>
    <insert id="save" parameterType="edu.scnu.bean.User">
        <!-- 生成主键的顺序Mysql是之后,oracle是之前 -->
        <selectKey keyProperty="id" resultType="Integer" order="AFTER">
            select LAST_INSERT_ID()
        </selectKey>
        insert into mb_user (username,birthday,sex,address) values (#{username}, #{birthday}, #{sex}, #{address})
    </insert>
</mapper>

接下来如果把实现类删除了,我们发现测试类实例化就报错了,这时候我们需要通过session的getMapper方法获得Mybatis帮我们创建好的实现类

测试类:

@Before
public void before() throws Exception{
    String resource = "sqlMapConfig.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();
    userDao = session.getMapper(UserDao.class);
}

优化配置文件

到目前为止,我们书写了两个配置文件,这两个配置文件开始出现不仅繁杂不好维护的问题,还有挺多不规范的地方:

  1. 数据库连接参数不应该直接在xml配置当中,应该提取出来:

使用property属性

<!-- 是用resource属性加载外部配置文件 -->
<properties resource="db.properties">
    <!-- 在properties内部用property定义属性 -->
    <!-- 如果外部配置文件有该属性,则内部定义属性被外部属性覆盖 -->
    <property name="jdbc.password" value="root" />
</properties>
<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>
  1. 类名过长重复

使用typeAliases属性自定义别名

mybatis已经为我们提供了很多基本数据类型以及包装类的别名,同样,我们也可以自己自定义类名别名

支持单个别名的定义

<typeAliases>
    <!-- 单个别名定义 -->
<typeAlias alias="user" type="edu.scnu.bean.User" />
</typeAliases>

还有批量定义,这里选用批量定义的方式

<typeAliases>
    <!-- 批量别名定义,扫描整个包下的类,别名为类名(大小写不敏感) -->
    <package name="edu.scnu.bean" />
</typeAliases>

这样后续的所有ResultType 和 parameterType只需要直接写User或user即可

诡异的是,也是需要注意的是顺序问题

typeAliases 必须紧跟 property 后

关于Mappers映射器属性

resource

  • 使用相对于类路径的资源(现在的使用方式)
    • 如:

class

  • 使用mapper接口类路径
    • 如:
    • 注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。

  • 注册指定包下的所有mapper接口
    • 如:
    • 注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。
    • 用于批量引入

输入类型和输出类型

接下来我们来完善其他业务功能,这里为了演示以下各类几种输入和输出类型的情况,我们增加一个实体类型包装实体

输入类型:

  • 基本类型
  • 实体类型
  • 实体类型包装类

说白了传递引用类型,会自动把所有引用属性转化为map对象,这点和struts的OGNL表达式类似

输出类型:

  • 基本类型
  • 实体
  • 实体列表
    • 只需要传递泛型类型(集合元素的类型即可)

然后来完善我们其他功能

parameterType为实体包装类的情况

<update id="update" parameterType="UserWrapper">
    update mb_user set username=#{user.username}, birthday=#{user.birthday}, sex=#{user.sex}, address=#{user.address} where id=#{user.id}
</update>

resultType为基本类型的情况

<select id="getCount" resultType="int">
    select count(*) from mb_user
</select>

动态sql完成多条件查询

使用where标签和if标签

<select id="getByCond" parameterType="User" resultType="User">
    select * from mb_user
    <where>
        <if test="sex != null and sex != ''">
            and sex = #{sex}
        </if>
        <if test="username != null and username != ''">
            and username = #{username}
        </if>
        <if test="address != null and address != ''">
            and address = #{address}
        </if>
    </where>
</select>

使用sql片段简化

sql标签声明 include引入

<sql id="selectSql">
    select * from ub_user
</sql>
<select id="find" resultType="User" parameterType="Integer">
    <include refid="selectSql" /> where id = #{id}
</select>

使用foreach标签实现多id查询

原理

select * from mb_user where id in (1,2,3);

以java数组传递参数

<select id="getByIds" parameterType="Integer" resultType="User">
    <include refid="selectSql" />
    <where>
        <foreach collection="array" item ="id" separator="," open="id in (" close=")">
            #{id}
        </foreach>
    </where>
</select>

以list集合传递参数

<select id="getByIds" parameterType="Integer" resultType="User">
    <include refid="selectSql" />
    <where>
        <foreach collection="list" item ="id" separator="," open="id in (" close=")">
            #{id}
        </foreach>
    </where>
</select>

resultMap 输出映射,完成订单信息

之前我们说到,订单Dao类是以驼峰命名的,而数据库字段是下划线命名的

在之前同名的情况都可以自动映射,那么针对这种不同名的情况又如何呢?

我们先简单完成以下订单的其中一个业务

public interface OrdersDao {
    List<Orders> getAll();
}

用户订单关联查询

一对一

方式一、使用新建实体类继承关系

新建一个OrdersUser 的javaBean类

public class OrdersUser extends Orders {
    private String username;
    private String address;
    // ...
}

配置OrdersUser.xml

<mapper namespace="edu.scnu.dao.OrdersUserDao">
    <select id="queryAll" resultType="OrdersUser">
        SELECT
            o.id,
            o.user_id
            userId,
            o.number,
            o.createtime,
            o.note,
            u.username,
            u.address
        FROM
            `order` o
        LEFT JOIN `user` u ON o.user_id = u.id

    </select>

</mapper>

方式二、直接多对一关系映射

直接在Orders实体类下增加User字段,并在配置文件里作如下映射:

<resultMap type="Orders" id="orderResultMap">
    <!-- 定义主键 ,非常重要。如果是多个字段,则定义多个id -->
    <id property="id" column="id"></id>
    <result property="userId" column="user_id"/>
    <!-- 关联,属性全写上 -->
    <result property="number" column="number" />
    <result property="createtime" column="createtime" />
    <result property="note" column="note" />
    
    
    <association property="user" javaType="User">
        <!-- id:声明主键,表示user_id是关联查询对象的唯一标识-->
        <id property="id" column="user_id" />
        <result property="username" column="username" />
        <result property="address" column="address" />
    </association>
</resultMap>

一对多关系

一对多关系只能使用resultMap的collection映射

同样在User Bean类中添加List<Orders>字段

private List<Orders> orders;

<resultMap type="User" id="UserOrdersResultMap">
    <id property="id" column="id" />
    <result property="username" column="username"/>
    <result property="address" column="address"/>
    <collection property="orders" javaType="List" ofType="Orders">
        <result property="id" column="oid"/>
        <result property="createtime" column="createtime"/>
        <result property="number" column="number"/>
    </collection>
</resultMap>
<select id="getWithOrders" resultMap="UserOrdersResultMap">
    SELECT
        u.id,
        u.username,
        u.address,
        o.id oid,
        o.number,
        o.createtime
    FROM
        `mb_user` u
    LEFT JOIN `mb_orders` o ON u.id = o.user_id
</select>

结束

好了,到这里已经介绍了单独使用Mybatis操纵的所有功能,整个流程到这里就算完成了,整个流程的详细项目代码可以踩我的github结合观光:https://github.com/Autom-liu/MybatisLearn ,整个流程涉及到诸多知识细节需要慢慢琢磨,这里只给大家展示整个概貌,具体细节会在后续的文章中慢慢解释,还会给大家展示Mybatis和Spring整合后的精彩内容哦,期待大家的支持和star~~~