使用Mybatis完成CRUD操作,并从源码分析执行过程

一、方式1:使用代理DAO

------>项目源码
本文接着Mybatis快速入门的案例,进行其他CRUD操作,使用的是Mapper接口代理DAO的开发方式。

重点其实就是映射文件的配置,在写SQL 语句传参时,使用标签的 parameterType 属性来设定参数类型。
该属性的取值可以是基本类型,引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。

案例的目标表:
在这里插入图片描述
实体类:
在这里插入图片描述
目录结构:
在这里插入图片描述

插入操作

首先在dao的接口中添加:

    /**
     * 保存用户
     *
     * @param user
     */
    void saveUser(User user);

在映射配置中:

    <!-- 保存用户 -->
    <insert id="saveUser" parameterType="com.zhu.damain.User">
        <!-- 配置插入操作后,获取插入数据的id -->
        insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
    </insert>

测试类:

/**
 *
 * 测试mybatis的crud操作
 */
public class MybatisTest {

    private InputStream in;
    private SqlSession sqlSession;
    private IUserDao userDao;

    @Before
    public void init()throws Exception{
        //1.读取配置文件,生成字节输入流
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.获取SqlSessionFactory
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        //3.获取SqlSession对象
        sqlSession = factory.openSession();
        //4.获取dao的代理对象!!!
        userDao = sqlSession.getMapper(IUserDao.class);
    }

    @After
    public void destroy()throws Exception{
        //提交事务
        sqlSession.commit();
        //6.释放资源
        sqlSession.close();
        in.close();
    }

    /**
     * 测试查询所有
     */
    @Test
    public void testFindAll(){
        //5.执行查询所有方法
        List<User> users = userDao.findAll();
        for(User user : users){
            System.out.println(user);
        }

    }

    /**
     * 测试保存操作
     */
    @Test
    public void testSave(){
        User user = new User();
        user.setUserName("modify User property");
        user.setUserAddress("北京市顺义区");
        user.setUserSex("男");
        user.setUserBirthday(new Date());
        System.out.println("保存操作之前:"+user);
        //5.执行保存方法
        userDao.saveUser(user);

        System.out.println("保存操作之后:"+user);
    }
}

注意: 需要在执行SQL后,通过sqlSession.commit()手动提交事务。
特别的,factory.openSession(true)可以将事务类型改为自动提交,这样就不需要手动提交了。

获取插入数据的id

这个功能可以通过这条sql语句实现: select last_insert_id();
如果通过Mybatis实现呢?
可以在映射配置XML中,在原来的基础上添加:

    <!-- 保存用户 -->
    <insert id="saveUser" parameterType="com.zhu.domain.User">
        <!-- 配置插入操作后,获取插入数据的id -->
        <selectKey keyProperty="userId" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey>
        insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
    </insert>

keyProperty为实体类的属性名, keyColumn为表的列名,order为语句执行的时机(在insert之前或之后)。
那么测试类中测试一下:

    /**
     * 测试保存操作
     */
    @Test
    public void testSave(){
        User user = new User();
        user.setUserName("modify User property");
        user.setUserAddress("北京市顺义区");
        user.setUserSex("男");
        user.setUserBirthday(new Date());
        System.out.println("保存操作之前:"+user);
        //5.执行保存方法
        userDao.saveUser(user);

        System.out.println("保存操作之后:"+user);
    }

可以看到,实体类的id属性在保存后被赋值了。

更新操作

在dao的接口中添加:

    /**
     * 更新用户
     *
     * @param user
     */
    void updateUser(User user);

在映射配置中添加:

   <!-- 更新用户 -->
    <update id="updateUser" parameterType="com.zhu.damain.User">
        update user set username=#{userName},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id}
    </update>

在测试类中添加:

    /**
     * 测试更新操作
     */
    @Test
    public void testUpdate(){
        User user = new User();
        user.setUserId(50);
        user.setUserName("mybastis update user");
        user.setUserAddress("北京市顺义区");
        user.setUserSex("女");
        user.setUserBirthday(new Date());

        //5.执行更新方法
        userDao.updateUser(user);
    }

删除操作

在dao的接口中添加:

    /**
     * 根据Id删除用户
     *
     * @param userId
     */
    void deleteUser(Integer userId);

在映射配置中添加:

    <!-- 删除用户-->
    <delete id="deleteUser" parameterType="java.lang.Integer">
        delete from user where id = #{uid}
    </delete>

这里需要注意的是由于SQL语句中只需要一个参数,并且是基本类型或其包装类,所以#{ }中的内容可以任意写。

测试类:

    /**
     * 测试删除操作
     */
    @Test
    public void testDelete(){
        //5.执行删除方法
        userDao.deleteUser(48);
    }

查询单个

dao接口添加:

    /**
     * 根据id查询用户信息
     *
     * @param userId
     * @return
     */
    User findById(Integer userId);

映射配置:

    <!-- 根据id查询用户 -->
    <select id="findById" parameterType="INT" resultType="com.zhu.domain.User">
        select * from user where id = #{uid}
    </select>

测试类:

    /**
     * 测试查询操作
     */
    @Test
    public void testFindOne(){
        //5.执行查询一个方法
        User user = userDao.findById(50);
        System.out.println(user);
    }

模糊查询

dao接口添加:

    /**
     * 根据名称模糊查询用户信息
     *
     * @param username
     * @return
     */
    List<User> findByName(String username);

映射配置:

    <!-- 根据名称模糊查询 -->
    <select id="findByName" parameterType="string" resultMap="com.zhu.domain.User">
          select * from user where username like #{name}
   </select>

测试类添加:

    /**
     * 测试模糊查询操作
     */
    @Test
    public void testFindByName(){
        //5.执行查询一个方法
        List<User> users = userDao.findByName("%王%");
        for(User user : users){
            System.out.println(user);
        }
    }

我们在配置文件中没有加入%来作为模糊查询的条件,所以在传入字符串实参时,就需要给定模糊查询的标识%。

聚合查询

dao接口添加:

    /**
     * 查询总用户数
     *
     * @return
     */
    int findTotal();

映射配置:

    <!-- 获取用户的总记录条数 -->
    <select id="findTotal" resultType="int">
        select count(id) from user;
    </select>

测试类:

    /**
     * 测试查询总记录条数
     */
    @Test
    public void testFindTotal(){
        //5.执行查询一个方法
        int count = userDao.findTotal();
        System.out.println(count);
    }

二、查询条件封装

在上面的CRUD中,可以明白parameterType属性的取值可以是基本类型,引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。不仅如此,该取值还可以是实体类的封装类。这是通过OGNL(Object-Graph Navigation Language,对象-图导航语言)表达式来实现的。它通过对象的getter方法来获取数据,在写法上把get省略了。

例如我们要获取用户名称:
类中的写法:user.gerUsername();
OGNL表达式的写法:user.username

若查询条件是综合的查询条件,不仅包括用户查询条件还包括其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用封装对象传递输入参数。

例如,现在需要根据用户名查询用户信息,查询条件放到 QueryVo 的 user 属性中。QueryVo 就是一个封装对象。

/**
 * 封装对象
 */
public class QueryVo {

    private User user;
	private Product product;

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public Product setProduct() {
        this.product = product;
    }

    public void getProduct() {
        return product;
    }
}

dao接口定义查询方法:

    /**
     * 根据queryVo中的条件查询用户
     *
     * @param vo
     * @return
     */
    List<User> findUserByVo(QueryVo vo);

映射配置:
语句中使用了OGNL表达式——

    <!-- 根据queryVo的条件查询用户 -->
    <select id="findUserByVo" parameterType="com.zhu.domain.QueryVo" resultType="com.zhu.domain.User">
        select * from user where username like #{user.username}
    </select>

测试类:

    /**
     * 测试使用QueryVo作为查询条件
     */
    @Test
    public void testFindByVo(){
        QueryVo vo = new QueryVo();
        User user = new User();
        user.setUserName("%王%");
        vo.setUser(user);
        //5.执行查询一个方法
        List<User> users = userDao.findUserByVo(vo);
        for(User u : users){
            System.out.println(u);
        }
    }

三、查询结果封装

之前案例中都强调实体类的属性要与表的列名保持一致,万一由于某种原因改动了实体类属性(id -> userId),造成了不一致的情况,那么查询到的数据就无法封装进实体对象了,那么应该如何解决呢?

  • 别名
    <!-- 查询所有 -->
    <select id="findAll" parameterType="com.zhu.damain.User">
        select id as userId,username as userName,address as userAddress,sex as userSex,birthday as userBirthday from user;
    </select>
  • 在映射配置中使用resultMap标签:
    <!-- 配置 查询结果的列名和实体类的属性名的对应关系 -->
    <resultMap id="userMap" type="com.zhu.domain.User">
        <!-- 主键字段的对应 -->
        <id property="userId" column="id"></id>
        <!--非主键字段的对应-->
        <result property="userName" column="username"></result>
        <result property="userAddress" column="address"></result>
        <result property="userSex" column="sex"></result>
        <result property="userBirthday" column="birthday"></result>
    </resultMap>

id 属性:给定一个唯一标识,提供给 select 标签引用用的
type 属性:指定实体类的全限定类名

接着在select标签中引入:

    <!-- 查询所有 -->
    <select id="findAll" resultMap="userMap">
        select * from user;
    </select>

虽然比别名的方式多配了一段xml,但是好处是一次配置就能多处使用。

四、主配置文件中的两个技巧

propertie标签的使用及细节

之前,在主配置文件中,我们是这么写的:
在这里插入图片描述
下面介绍propertie标签的另一种用法。
1.先在 classpath 下定义 db.properties 文件:
在这里插入图片描述

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root
  1. 在主配置xml中配置properties标签:

	<properties resource="jdbcConfig.properties">

    </properties>

resource 属性:用于指定 properties 配置文件的位置,要求配置文件必须在类路径下

	<properties url="file:///E:/mybatis_learning/mybatis_4_CRUD/src/main/resources/jdbcConfig.properties">

    </properties>
  1. 修改dataSource标签:
            <!--配置连接池-->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"></property>
                <property name="url" value="${jdbc.url}"></property>
                <property name="username" value="${jdbc.username}"></property>
                <property name="password" value="${jdbc.password}"></property>
            </dataSource>

显而易见,这样的方式可以更灵活的进行配置。

typeAliases(类型别名)与 package

  • Mybatis 支持默认别名,我们还可以使用typeAliases标签自定义别名。
    <!--使用typeAliases配置别名,它只能配置domain中类的别名 -->
    <typeAliases>
    	<!--typeAlias用于配置别名。type属性指定的是实体类全限定类名。alias属性指定别名,当指定了别名就不再区分大小写 -->
        <typeAlias type="com.zhu.domain.User" alias="user"></typeAlias>-->
    </typeAliases>

定义别名后,在映射配置中就可以这么简写了,而不再需要写全限定类名:
在这里插入图片描述

  • 批量自定义别名:package标签
    <typeAliases>
        <!-- 用于指定要配置别名的包,当指定之后,该包下的实体类都会注册别名,并且类名就是别名,不再区分大小写-->
        <package name="com.zhu.domain"></package>
    </typeAliases>

package标签也能用于主配置的标签中:

    <!-- 配置映射文件的位置 -->
    <mappers>
        <!--<mapper resource="com/zhu/dao/IUserDao.xml"></mapper>-->
        <!-- package标签是用于指定dao接口所在的包,当指定了之后就不需要在写mapper以及resource或者class了 -->
        <package name="com.zhu.dao"></package>
    </mappers>

此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。
在这里插入图片描述

五、方式2:编写 Dao 实现类

------->项目源码
使用 Mybatis 开发 Dao,通常有两个方法,即原始 Dao 开发方式和 Mapper 接口代理开发方式。 而现在主流的开发方式是接口代理开发方式,这种方式总体上更加简便。以上的案例就是接口代理开发的方式,现在通过案例,学习基于传统编写 Dao 实现类的开发方式。

各项配置还是沿用之前的。

查询列表

由于现在需要自己实现dao接口,那么先写一个实现类:
在这里插入图片描述
先实现findAll方法:

/**
 * Dao实现类
 */
public class UserDaoImpl implements IUserDao {

    private SqlSessionFactory factory;

    public UserDaoImpl(SqlSessionFactory factory){
        this.factory = factory;
    }

    @Override
    public List<User> findAll() {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询列表
        List<User> users = session.selectList("com.zhu.dao.IUserDao.findAll");//参数就是能获取配置信息的key
        //3.释放资源
        session.close();
        return users;
    }

测试类与之前最主要的区别就是,SqlSession对象此时是在自己的dao实现类中使用了,因此测试类中不再需要获取:

/**
 *
 * 测试mybatis的crud操作
 */
public class MybatisTest {

    private InputStream in;
    private IUserDao userDao;

    @Before
    public void init()throws Exception{
        //1.读取配置文件,生成字节输入流
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.获取SqlSessionFactory
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        //3.使用工厂对象,创建dao对象
        userDao = new UserDaoImpl(factory);
    }

    @After
    public void destroy()throws Exception{
        //6.释放资源
        in.close();
    }

    /**
     * 测试查询所有
     */
    @Test
    public void testFindAll(){
        //5.执行查询所有方法
        List<User> users = userDao.findAll();
        for(User user : users){
            System.out.println(user);
        }

    }

测试结果:
在这里插入图片描述

插入操作

实现类中添加:

    @Override
    public void saveUser(User user) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用方法实现保存
        session.insert("com.zhu.dao.IUserDao.saveUser",user);
        //3.提交事务
        session.commit();
        //4.释放资源
        session.close();
    }

别忘了提交事务,以及insert方法需要传一个来源对象。

测试类添加:

    /**
     * 测试保存操作
     */
    @Test
    public void testSave(){
        User user = new User();
        user.setUsername("dao impl user");
        user.setAddress("广州市天河区");
        user.setSex("男");
        user.setBirthday(new Date());
        System.out.println("保存操作之前:"+user);
        //5.执行保存方法
        userDao.saveUser(user);

        System.out.println("保存操作之后:"+user);
    }

测试结果:
在这里插入图片描述

更新、删除、查询单个、聚合查询

代码结构都与之前的类似,注意查询单个使用的是session.selectOne
实现类:

    @Override
    public void updateUser(User user) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用方法实现更新
        session.update("com.zhu.dao.IUserDao.updateUser",user);
        //3.提交事务
        session.commit();
        //4.释放资源
        session.close();
    }

    @Override
    public void deleteUser(Integer userId) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用方法实现更新
        session.update("com.zhu.dao.IUserDao.deleteUser",userId);
        //3.提交事务
        session.commit();
        //4.释放资源
        session.close();
    }

    @Override
    public User findById(Integer userId) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询一个
        User user = session.selectOne("com.zhu.dao.IUserDao.findById",userId);
        //3.释放资源
        session.close();
        return user;
    }

    @Override
    public List<User> findByName(String username) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询列表
        List<User> users = session.selectList("com.zhu.dao.IUserDao.findByName",username);
        //3.释放资源
        session.close();
        return users;
    }

    @Override
    public int findTotal() {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询一个
        Integer count = session.selectOne("com.zhu.dao.IUserDao.findTotal");
        //3.释放资源
        session.close();
        return count;
    }

测试类:

 /**
     * 测试更新操作
     */
    @Test
    public void testUpdate(){
        User user = new User();
        user.setId(8);
        user.setUsername("userdaoimpl update user");
        user.setAddress("上海市黄浦区");
        user.setSex("女");
        user.setBirthday(new Date());

        //5.执行保存方法
        userDao.updateUser(user);
    }

    /**
     * 测试删除操作
     */
    @Test
    public void testDelete(){
        //5.执行删除方法
        userDao.deleteUser(8);
    }

    /**
     * 测试查询单个操作
     */
    @Test
    public void testFindOne(){
        //5.执行查询一个方法
        User  user = userDao.findById(5);
        System.out.println(user);
    }

    /**
     * 测试模糊查询操作
     */
    @Test
    public void testFindByName(){
        //5.执行查询一个方法
        List<User> users = userDao.findByName("%东%");
        for(User user : users){
            System.out.println(user);
        }
    }

    /**
     * 测试查询总记录条数
     */
    @Test
    public void testFindTotal(){
        //5.执行查询一个方法
        int count = userDao.findTotal();
        System.out.println(count);
    }

分别测试——
更新:
在这里插入图片描述
删除:

在这里插入图片描述
查询单个:
在这里插入图片描述
模糊查询:在这里插入图片描述
查询总记录:
在这里插入图片描述

小结

通过对比可以看出来,通过DAO实现类的开发方式,需要多写许多代码,因此这种方式用得并不多。
深究其原因,可以通过下文的分析执行过程来得出结论。

六、从源码分析Mybatis的CRUD执行过程

下面分析对于自己写DAO实现类的这种开发方式,mybatis内部的执行细节。

以分析查询列表方法为例,从我们的实现类开始,点击session.selectList()查看源码,当进入一个接口且它有多个实现类时,可以通过断点调试的方法来判断执行过程中调用的是哪一个实现类,这样就能继续深入追踪了。

分析selectList()方法

session.selectList()开始,一路追踪,最终到 PreparedStatementHandler这个类为止,可以整理出从selectList()方法直到ps.execute()所调用的所有的类及其方法(点击查看大图更清晰):
在这里插入图片描述

PreparedStatementHandler.query的代码如下,最终执行sql的,不就是我们在JDBC中熟悉的 preparedStatement.execute()吗?

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

读代码可知,执行完execute()之后,再通过handleResultSets进行结果集的封装,追踪到这里,mybatis内部如何执行selectList()方法,已经不再神秘!

附上涉及到的部分类图:
在这里插入图片描述
在这里插入图片描述

分析其他方法

insert()、update()、delete()

为了方便归纳,实体类进行删除操作时,原先的update改为通过delete方法完成:
在这里插入图片描述
接下来如法炮制,分析session.insert()session.update()session.delete()三个方法。为什么要把这三个放在一起呢?因为它们在DefaultSqlSession中,其实调用的都是同一个方法:update(),看下图就明白了(点击查看大图更清晰):
在这里插入图片描述
同样,一路追踪,最终到 PreparedStatementHandler这个类,执行update方法:

  @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

综上,尽管mybatis为我们提供了session.insert()session.update()session.delete()这些方法,但是其底层都是通过preparedStatement.execute()来实现最终对数据库的SQL操作的。

selectOne()

同上,可以很快发现,session.selectOne()方法,会在DefaultSqlSession中调用selectList()方法,其后的流程上面已经分析过了。
在这里插入图片描述

七、从源码分析Mybatis的CRUD执行过程(代理DAO方式)

上面分析的是DAO实现类的方式,现在对更为常用的代理DAO方式进行分析(点击查看大图更清晰)。
在这里插入图片描述
从测试类开始,一路追踪到MapperMethod对象中,最终execute()方法执行的是session.insert()session.update()session.delete()三个熟悉的方法,看了上文的分析,我们已经很清楚,至此之后的执行过程和自己写DAO实现类的实现方式是一模一样的。稍有不同的sqlSession.selectList()方法也在同一个类中的executeForMany()方法中被调用,同样,又与上文的分析连接到了一起。

小结

为什么Mybatis能让我们无需写DAO实现类?
因为它已经帮我们调用了,当我们自己写DAO实现类时所调用的方法!
既然Mybatis已经帮我们做了这些事情,那我们就没有理由再做一遍——这就是为什么大多使用DAO代理的方式来进行开发的理由。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值