likes
comments
collection
share

一文搞懂MyBatis的使用

作者站长头像
站长
· 阅读数 41

在学习Java的过程,我们可能使用了JDBC来访问数据库获取数据,但是JDBC也有比较大的问题

  1. sql语句写在java程序中,需求修改的话则需要修改Java代码
  2. sql占位符的拼接传递比较麻烦
  3. 数据库对象映射成Pojo也比较麻烦

MyBatis这个框架通过对JDBC的封装,开发者只需要按照约定的配置进行配置开发,就可以方便的进行ORM操作

O(Object),R(Relational),M(Mapping):对象关系映射,大白话就是Java对象映射成数据库某张表中的一行记录,一行记录又可映射成一个Java对象

开始上车

安装Mybatis,传统的模式自然免不了找到对应的Jar包添加成依赖,但通过Maven工具可以方便的控制项目依赖,需要在pom.xml中添加对应依赖,关于Maven这里不介绍过多,不知道GVC怎么写的可以在mvnrepository.com/ maven仓库中搜索对应的即可,比如搜索MyBatis,选择对应的版本,复制如图所示的依赖配置到pom.xml中,刷新maven依赖即可下载,这里除了Mybatis还需要下载mysql-connectorJava链接数据库需要通过驱动,不同厂商有各自的实现

一文搞懂MyBatis的使用

创建一个Maven项目,创建过程忽略

这里以Mysql为例子,在maven依赖中添加如下依赖,一个是mybatis,一个是mysql的驱动

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.11</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

按照官网提供的配置进行本地化配置

  1. 先配置一份mybatis的主配置文件
  2. 下面的配置为最简化的配置,需要替换掉dataSource中的四个标签值,并且这里我选择了创建一个jdbc.properties文件用来管理数据库链接信息,通过properties标签的resource属性进行导入,需要注意文件的存放位置,默认会从resource目录开始找(如果不用这种方式,直接写上一个字符串即可)
<?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="jdbc.properties"></properties>
  <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="mapper/UserMapper.xml"/>
  </mappers>
</configuration>

jdbc.properties创建,自行替换

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/db
username=root
password=12345678

UserMapper.xml创建,这里需要说明一下,mapper标签中的namespace属性,作用类似于唯一包名,主要用于区分不同mapper文件中,增删改查标签中id相同的情况,如有多个mapper文件,里面的查询标签id一致,但是加上namespace.进行拼接就可以区分。这里主要演示mybatis,对应的表设计可和sql自行处理,这里提供参考(学习Mybatis的使用才是文章内容,不了MysqlSql的应该先去了解)

一文搞懂MyBatis的使用

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="user">
    <insert id="insertUser">
        insert into t_user
            (`username`, `password`, `age`, `gender`)
        values ('ben', '123', 40, '0')
    </insert>
</mapper>

目前resources目录下应该有如下文件

一文搞懂MyBatis的使用

编写一个测试类来验证一个最基础的mybatis使用

  1. 需要创建一个SqlSessionFactoryBuilder对象
  2. 通过SqlSessionFactoryBuilder对象的build方法创建SqlSessionFactory对象,需要传入一个InpuStream类型的参数,实际上是指上面编写好的mybatis-config.xml
  3. 通过SqlSessionFactoryopenSession方法创建SqlSession对象,方法可以接受一个boolean参数,由于配置中配置的是JDBC的事务管理器,所以事务默认是不提交的,可以通过传入true来开启自动提交(可以理解为执行一次增删改就自动提交一次),不传或传入false则表示开启事务(得手动提交)
  4. 由于需要执行的是插入数据操作,所以调用sqlSession.insert方法(会有对应的updatedelete方法等),需要传入一个唯一id,这个id指的就是UserMapper.xmlmapper标签下配置的insert标签的id,如果成功,则会返回影响的数据条数
public class Main {
    public static void main(String[] args) throws IOException {
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory build = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
        // SqlSession sqlSession = build.openSession(true); // 如果传入true则开启自动提交
        SqlSession sqlSession = build.openSession();
        try{
            // 像目前只有一个mapper文件,可以不用加前缀,直接insertUser即可,否则最好加上namespace避免重名
            int count = sqlSession.insert("user.insertUser"); // 会返回影响的条数
            if (count == 1) {
                System.out.println("插入成功成功");
            }else{
                System.out.println("插入数据失败");
            }
        	sqlSession.commit(); // 记得要提交事务
        }catch(Exception e){
            // 抛出异常时事务回滚
            if(sqlSession != null){
                sqlSession.rollback();
            }
        }finally{
            // 结束时关闭资源
            if(sqlSession != null){
                sqlSession.close();
            }
        }
    }
}

当执行程序后,可以看到数据库成功增加了一条记录

一文搞懂MyBatis的使用

关于SqlSessionFactoryBuilder的入参输入流,代码演示使用了mybatis提供的一个工具类,如果你想,也可以替换成任何自己实现的InputStream对象,只要能创建即可

  1. new FileInputStream("xxxx");
  2. ClassLoader.getSystemClassLoader().getResourceAsStream("xxxx");实际上,MyBatis提供的方法底层就是通过这个ClassLoader对象进行调用的,原理是从类路径开始查找对应资源
  3. ...任何产出InputStream的方式

关于日志配置

日志的输出可以让开发过程更清晰的看到SQL的拼接执行等信息,这里需要十分注意settings标签的顺序位置,错了的话会报错,具体顺序报错信息会提示你

    <properties resource="jdbc.properties"></properties>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"></setting>
    </settings>
    <environments default="development">

这里使用了mybatis内置的标准日志实现,添加配置后再次执行,就能看到如图所示的日志信息,方便开发调试

一文搞懂MyBatis的使用

亦可配置如SLF4J,Log4j等,其中的区别可能是性能或配置需求等,自行抉择

动态插入

上面的实例中,插入语句是写死的值,显然实际开发中对应的值是需要前端将用户注册的参数穿过来,此时可以使用#{}进行占位,作用等价于JDBC中的?占位符(底层用的是PreparedStatement没有SQL注入问题)

  1. 执行对应方法时,传入Map集合
  2. 传入Pojo对象,但是需要编写对应的getter方法

测试insert传入Map

先改写mapper.xml,将原本写死的地方改成#{Map的key名}

<mapper namespace="user">
    <insert id="insertUser">
        insert into t_user
            (username, password, age, gender)
        values (#{username}, #{password}, #{age}, #{gender})
    </insert>
</mapper>
public class Main {
    public static void main(String[] args) throws IOException {
        // 简单的工具类封装了SqlSession
        SqlSession sqlSession = SqlSessionUtil.openSqlSession();
        Map<String, Object> user = new HashMap();
        user.put("username", "jack");
        user.put("password", "jinwandalaohu");
        user.put("age", 60);
        user.put("gender", "0");
        int count = sqlSession.insert("user.insertUser", user);
        if (count == 1) {
            System.out.println("插入成功");
        } else {
            System.out.println("insert failed");
        }
        sqlSession.close();
    }
}

再次执行,可以看到#{}被替换成了?,并且日志输出了每个占位符对应的值和数据类型,并且数据成功插入

一文搞懂MyBatis的使用

需要注意的是,如果使用Map当参数传入,占位符填写了不存在的key,则最终插入的值会是null

测试传入Pojo类

先建立一个User类,比较关键的地方在于,需要编写对应的getter方法

public class User {
    private Long id;
    private String username;
    private String password;
    private Character gender;
    private Integer age;

    public User() {
    }

    public User(String username, String password, Character gender, Integer age) {
        this.username = username;
        this.password = password;
        this.gender = gender;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Character getGender() {
        return gender;
    }

    public void setGender(Character gender) {
        this.gender = gender;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

Usermapper.xml的内容不需要修改

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSession sqlSession = SqlSessionUtil.openSqlSession();
        User user = new User("Susan", "qweasd", '1', 29);
        int count = sqlSession.insert("user.insertUser", user);
        if (count == 1) {
            System.out.println("插入成功");
        } else {
            System.out.println("insert failed");
        }
        sqlSession.close();
    }
}

执行后依然可以看到插入成功

一文搞懂MyBatis的使用

使用Pojo类和Map最大的区别点在于,必须传入存在的getter方法名,举个例子,Mybatis会把getUsernameget去掉,首字母U改成小写的u,最终占位符传入的就是username,如果不存在getter则会报错,而不是传入null

Mapper的编写中,有一个parameterType参数类型可以传入全限定类名,但一般这个可以忽略不写,如果要写格式如下

<insert id="insertUser" parameterType="cn.mgl.pojo.User">

动态删除

UserMapper.xml中,添加一个delete标签,由于传参是个简单数据类型,且只有一个参数,占位符的key可以随便写

<mapper namespace="user">
    <delete id="deleteUser">
        delete from t_user where id = #{suibiannixie}
    </delete>
</mapper>

调用sqlSessiondelete方法,传入id

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSession sqlSession = SqlSessionUtil.openSqlSession();
        int count = sqlSession.delete("user.deleteUser",8);
        if (count == 1) {
            System.out.println("删除成功");
        } else {
            System.out.println("delete failed");
        }
        sqlSession.close();
    }
}

可以看到8被成功替换了#{suibiannixie}

一文搞懂MyBatis的使用

动态修改

这里演示只根据用户id修改用户名

<mapper namespace="user">
  <update id="updateUser">
    update t_user set username=#{username} where id = #{id}
  </update>
</mapper>

调用sqlSessionupdate方法,传入User对象

public static void main(String[] args) throws IOException {
    SqlSession sqlSession = SqlSessionUtil.openSqlSession();
    User user = new User();
    user.setUsername("tester");
    user.setId(14L);
    int count = sqlSession.delete("user.updateUser",user);
    if (count == 1) {
        System.out.println("更新用户名成功");
    } else {
        System.out.println("update failed");
    }
    sqlSession.close();
}

更新成功如下

一文搞懂MyBatis的使用

使用Mybatis查询数据

与增删改有点区别,他们都是返回数据表中被影响的数据行数,而查询则会返回一个封装好的查询结果集,比如查询用户表,那么返回结果就是单个用户对象,或者用户对象的集合

编写mapper,图方便用*

<mapper namespace="user">
  <select id="selectById">
    select * from t_user where id = #{id}
  </select>
</mapper>

执行sqlSessionselectOne方法

public static void main(String[] args) throws IOException {
    SqlSession sqlSession = SqlSessionUtil.openSqlSession();
    Object user = sqlSession.selectOne("user.selectById", 1);
    System.out.println(user);
    sqlSession.close();
}

执行程序后可以看到如下报错

一文搞懂MyBatis的使用

意思就是mybatis检测到你没有为select标签提供对应的结果映射类型,不提供的话它没有办法知道如何处理对应的结果集,再次修改mapper,添加一个resultType标签,填写全限定类名

<mapper namespace="user">
  <select id="selectById" resultType="cn.mgl.pojo.User">
    select * from t_user where id = #{id}
  </select>
</mapper>

一文搞懂MyBatis的使用

再次执行便能查到对应用户信息,这里需要注意一下几点

当用户表字段用Pojo类命名格式不一致

为了演示效果,为用户表额外添加一个underScoreCase(下划线命名)的字段,并给对应数据添加日期类型的值

一文搞懂MyBatis的使用

Pojo类中添加对应的字段和getter方法,java命名规范是小驼峰

一文搞懂MyBatis的使用

此时再次执行查询程序,可以看到对象映射并没有成功,对应的createdAt字段为null

一文搞懂MyBatis的使用

原因就是因为属性名没有对上,有以下几种解决方案

  1. 通过sql编写别名,让查询结果字段名对的上created_at as createdAt
<select id="selectById" resultType="cn.mgl.pojo.User">
  select id, username, password, gender, age, created_at as createdAt
  from t_user
  where id = #{id}
</select>
  1. 通过mybatis提供的设置选项,开启驼峰命名自动映射,前提是转换后的名字能对得上,如数据库的字段为a_b,转化后就成了aB
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"></setting>
        <setting name="logImpl" value="STDOUT_LOGGING"></setting>
    </settings>
  1. 通过select标签的resultMap映射(后面会说)

前两种方式任选其一配置后,再次执行查询语句,可以看到createdAt字段已经有值了

一文搞懂MyBatis的使用

查询多条数据

sqlSession查多条就需要调用selectList方法,虽然返回的是List集合,但resultType仍然指定实体类名,新增一个select标签

    <select id="selectAll" resultType="cn.mgl.pojo.User">
        select id, username, password, gender, age, created_at as createdAt
        from t_user
    </select>

调用selectList方法,接受类型为User集合

public static void main(String[] args) throws IOException {
    SqlSession sqlSession = SqlSessionUtil.openSqlSession();
    List<User> users = sqlSession.selectList("user.selectAll");
    users.forEach(System.out::println);
    sqlSession.close();
}

查询成功

一文搞懂MyBatis的使用

实际上,查询结果为单条的情况下也可以使用List进行接收

使用接口代理形式

上述的使用方式有一个非常大的问题在于,开发者需要记住且拼接对应的映射id,这种方式并不便于开发维护,所以Mybatis还提供了一个更为方便使用的代理模式

首先新建一个接口,这里命名为UserDao

public interface UserDao {
    int insertUser(User user);

    int deleteUser(Long id);

    int updateUser(User suer);

    User selectById(Long id);

    List<User> selectAll();
}

接着配置对应的mapper,这里需要注意的是,namespace不能再乱写了,需要写成UserDao的全限定类名,并且所有sql标签的id属性也要和接口中的方法名保持一致,如果不一致,则会抛出对应的异常

<mapper namespace="cn.mgl.dao.UserDao">
  <insert id="insertUser" parameterType="cn.mgl.pojo.User">
    insert into t_user
    (username, password, age, gender)
    values (#{username}, #{password}, #{age}, #{gender})
  </insert>
  <delete id="deleteUser">
    delete from t_user where id = #{suibiannixie}
  </delete>
  <update id="updateUser">
    update t_user set username=#{username} where id = #{id}
  </update>
  <select id="selectById" resultType="cn.mgl.pojo.User">
    select id, username, password, gender, age, created_at as createdAt
    from t_user
    where id = #{id}
  </select>
  <select id="selectAll" resultType="cn.mgl.pojo.User">
    select id, username, password, gender, age, created_at as createdAt
    from t_user
  </select>
</mapper>

调用执行的时候不再使用namespace拼接id的方式,而是使用sqlSessiongetMapper方法,传入对应的Mapper接口的类对象,mybatis底层实现会去进行解析反射等一系列操作,最终生成一个代理对象,开发者只需要调用接口中的方法即可完成数据库操作,实现的效果完全等价的

public static void main(String[] args) {
    SqlSession sqlSession = SqlSessionUtil.openSqlSession();
    UserDao mapper = sqlSession.getMapper(UserDao.class);
    List<User> users = mapper.selectAll();
    users.forEach(System.out::println);
    sqlSession.close();
}

这样的方式更为常用

关于#{}和${}

学过JDBC开发的都知道,有个PreparedStatementStatement对象

  • #{}底层实现对应着PreparedStatement,有防SQL注入的作用,先编译再进行占位符替换,上面所有的示例中都是使用这种方式,用的最多
  • ${}底层对应着Statement,有SQL注入的风险,一般在SQL语句需要对保留关键字进行拼接的情况下使用,能不用就不用

举几个需要使用${}的情况

  • 查询结果集需要根据username排序,排序方式需要自定义传入,编写如下SQL
<select id="selectAllOfSort" resultType="cn.mgl.pojo.User">
  select id, username, password, gender, age, created_at as createdAt
  from t_user order by username ${sort}
</select>

这种情况下才适合使用,引用如果使用#{},会将传入的asc或者desc加上字符的引号,那么对应的语句便不合法,因为关键字是不需要加引号的

  • 表名不能写死的情况下,需要动态修改执行,可以使用,原理还是引号导致的语法问题
  • SQL语句使用了in(????),如delete from xxx where id in (${keys})此时也可以使用,原理同上
  • 模糊查询的情况下,如select * from xxx where username like '%${key}'

模糊查询下如果不想使用${},可以选择#{},但是需要一定拼接技巧

  • 使用concat函数,语句可以这样写select * from xxx where username like concat('%',#{key},'%'),最终出来的效果就是合法的语句
  • 使用双引号包括%select * from xxx where username like "%"#{key}"%"

typeAliases 类型别名

主要用于给数据类型起别名,在mybatis-config.xml中进行配置,需要注意顺序,在后续的mapper文件中,指定类型时就不再需要写冗长的全限定类名,在使用的时候是不区分大小写的

    <typeAliases>
        <typeAlias type="cn.mgl.pojo.User" alias="User"/>
    </typeAliases>

像这个查询语句的resultType,原本写的全限定类名就可以替换成简短的别名

    <select id="selectAll" resultType="User">
        select id, username, password, gender, age, created_at as createdAt
        from t_user
    </select>

此时如果一个类名配置一次显然也会麻烦,恰好提供了一个package标签,只需要指定包名,那么这个包下的所有类都会生成一个对应的简类名别名供使用

    <typeAliases>
        <package name="cn.mgl.pojo"/>
    </typeAliases>

Mybatis配置文件配置项理解

其实这一环在官网的文档中有说明,遇到具体的配置问题可以随时查询 mybatis.org/mybatis-3/z…

一文搞懂MyBatis的使用

下面只简单的讲解

properties

提供了更灵活的配置使用,像上述demo中就通过资源文件动态配置了JDBC信息数据,可以使用resource或者url属性进行文件配置导入,也可使用子标签<property name="a" value="1"/>配置属性

  • 前提是资源文件在类路径下才能找到资源

environments

用于配置环境信息,有一个default属性,可以用于执行采用那个environment标签的配置信息,需要和对应标签的id一致

  • environment,这里配置的是具体的数据源信息,其中的id属性就是给其父标签environmentsdefault属性使用的
  • 一个environment可以理解为就是一个环境,对应着一个SqlSessionFactory对象

transactionManager

用于配置事务管理器,其中有个type属性可供使用

  1. JDBC,配置为这个值,则采用JDBC的事务机制,相当于默认开启事务并且不会自动提交,需要开发者手动的进行commit事务
  2. MANAGED,配置这个值会将事务机制交给其他容器管理,如果没有对应容器则没有事务,执行一次DML(增删改查)就提交一次

dataSource

用于指定数据源相关的信息,此标签有一个type属性可以配置连接池的策略,不同的type决定着子标签的property可以配置不同的属性值

  • UNPOOLED 采用最传统的方式获取数据库链接,并且不会有连接池的概念,每一次访问都是新的链接
    • driver 配置jdbc驱动的java全限定类名
    • url配置数据库的jdbc地址
    • username 配置数据库用户名
    • password 配置数据库用户密码
    • defaultTransactionlsolationLevel 配置默认的事务隔离级别(此处为Mysql的知识点)
    • defaultNetworkTimeout 配置等待数据库操作的默认网络超时时间
  • POOLED 采用了连接池的规范实现
    • 上述UNPOOLED策略列出的所有都可以配置
    • poolMaximumActiveConnections 在任意时间可以使用的最大连接数量,默认值是10,若一个请求占用一个线程,此时有20个请求,同一时间内只能处理10个请求,线程就会占满,第11个请求只能等待空闲线程
    • poolMaximumldleConnections 任意时间可能存在的空闲连接数,默认值是5,如果已经有个5个线程是空闲状态,当出现第6个空闲线程时,会将此线程关闭减少数据库开销
    • poolMaximumCheckoutTime 强制回归线程池时间,默认值为20s
    • poolTimetoWait 当无法获取到空闲连接时,每20s打印一次日志
    • 其他不太常用

property

该标签提供了更为灵活的配置方式,如将配置文件用其他文件格式的资源文件进行管理,上面第一个例子就是使用了引入外部资源的方式,配置resource属性

mapper

用于指定SQL映射文件路径

  1. 通过引入类路径下的资源文件
<mappers>
  <mapper resource="UserMapper.xml"/>
</mappers>
  1. 通过file:///格式的url进行查找,一般不会使用这个,移植性太差
  2. 通过Class接口的全限定类型进行配置,mybatis底层会进行动态代理,假设有这么一个UserMapper类,配置完后会自动进行映射,前提是对应的namespaceid等需要与类名方法名一一对应上,还有最关键一点:xml文件需要和接口类放到同一个目录下
<mappers>
  <mapper class="cn.mgl.mapper.UserMapper"/>
</mappers>
  1. 通过配置包名,这个是基于方式3的前提下,更便捷的配置映射,原本使用的mapper标签更改为package标签,整个包下的接口都会批量的进行映射
<mappers>
  <package name="cn.mgl.mapper"/>
</mappers>

传入数据后获取到自增的id

正常情况下,执行一个新增方法,会传入一个对应的实体类,但一般这个实体类是没有id的,如果需要插入成功后数据表中对应的自增id,要么自己根据条件再查询一遍获取,或者可以通过配置,在insert标签中开启useGeneratedKeys和指定propertKey

    <insert id="insertUser" parameterType="cn.mgl.pojo.User" useGeneratedKeys="true" keyProperty="id">
        insert into t_user
            (username, password, age, gender)
        values (#{username}, #{password}, #{age}, #{gender})
    </insert>

后续的使用中,没有添加上述属性时,传入的User实例是不会有id属性的,配置完了再次执行查看输出

public static void main(String[] args) {
    SqlSession sqlSession = SqlSessionUtil.openSqlSession();
    UserDao mapper = sqlSession.getMapper(UserDao.class);
    User user = new User("ma", "!23", '1', 20);
    mapper.insertUser(user);
    System.out.println(user);
    sqlSession.close();
}

可以看到id成功被赋值了

一文搞懂MyBatis的使用

Mybatis的参数处理

使用的代码示例当中,语句标签的parameterType上面说过可以省略,sql中使用的#{key}实际是个省略写法,他的完整写法如下

...sql忽略... where name=#{name, javaType=String, jdbcType=VARCHAR}

一般情况是都忽略不用写的,像7种基本数据类型以及对应的包装类,还有String,Date这些参数类型,mybatis会自己进行类型推导,最终由推导结果来觉得调用JDBC中的setString,setInt等等

多参数处理

之前演示的都是单参数形式,多参数的处理有一些规则需要遵守,先编写一个需要传入两个参数的sql及方法

<select id="giveTwoParameter" resultType="cn.mgl.pojo.User">
  select id, username, password, gender, age, created_at as createdAt
  from t_user
  where username = ${usernmae}
  and age = #{age}
</select>

新增一个接口方法

User giveTwoParameter(String username,Integer age);

执行方法,传入一个姓名和年龄

    public static void main(String[] args) {
        SqlSession sqlSession = SqlSessionUtil.openSqlSession();
        UserDao mapper = sqlSession.getMapper(UserDao.class);
        mapper.giveTwoParameter("ma",20);
        sqlSession.close();
    }

不出意外的话可以看到报错,提示username找不到,可以用的占位符key [arg1, arg0, param1, param2],需要说明一下,如果使用arg则后面的序号从0开始,如果使用param,则后面的序号从1开始

一文搞懂MyBatis的使用

只需要按照提示,将原本的#{username}#{age}替换成arg格式param格式的即可

实际上,mybatis在底层实现中,有一个Map集合进行数据收集

Map<String,Object> map = new HashMap<>();
map.put("arg0",key);
map.put("param1",key);
// 每个参数都以此类推

@Param注解的使用

MyBatis在代理生成对象时,会去检查入参的参数是否带有@Parmas的注解,如果有,则会新建一个Map集合用来存放最终的key and value,当使用了这个注解后,等价于把默认的arg格式key给替换成我们自定义的key,但param格式的key还保留着。修改接口中的参数,添加对应的注解值

    User giveTwoParameter(@Param("username") String username,@Param("age") Integer age);

对应的UserMapper.xml中占位符使用注解传入的key即可

可以尝试用Map去接受返回值

当不知道用什么对应的实体类接收SQL的返回值时,可以用Map类型,Mybatis会自动转换成键值对的形式,但一般不建议这么做,可读性太差了

当数据库字段与Java对象属性无法匹配时

  1. 可以通过sql语句的as关键字对需要查询出数据的列名进行重命名成Java属性对应的name
  2. 上面演示过的通过配置setting标签中的,驼峰转换自动映射,前提是数据库的列名和Java的属性名都遵循规范,如字段名为user_name,则对应的Java属性名为userName
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"></setting>
</settings>
  1. 通过定义resultMap来手动映射,以示例中出现过的的User类举例
    <resultMap id="customMap" type="cn.mgl.pojo.User">
        <id property="id" column="id"></id>
        <result property="age" column="age"></result>
        <result property="username" column="username"></result>
        <result property="password" column="password"></result>
        <result property="gender" column="gender"></result>
        <result property="age" column="age"></result>
        <result property="createdAt" column="created_at"></result>
    </resultMap>

    <select id="getByXxx" resultMap="customMap"></select>

这里的id为配置主键,其他的属性用result标签,其中propertyJava对象的属性,column为查询结果集的字段名,后续在指定select标签的返回值时,不再指定resultType,而是指定resultMap,传入的就是定义map时的id

Mybatis内置的类型

像一些常规的int、long,map,string等类型,不需要定义就可以在resultType中使用,这是因为框架内置了映射,具体的内置配置可以查看 mybatis.org/mybatis-3/z…,翻到类型别名处,文档又说明

Mybatis比较高级的特性--动态SQL

所谓的动态,就是能方便开发者进行一些有条件及便携的sql语句拼接

if标签

以一个常见的场景来说,需要查询某个数据列表,可以提供一些查询条件,这些条件可传可不传,此时就可以使用if标签了

    <select id="testIfLabel" parameterType="user">
        select * from t_user
        where 1 = 1
        <if test="gender != null">
            and gender = #{gender}
        </if>
    </select>

这里where语句后面的1=1是为了保证即使if标签的条件没有通过,也不会报语句错误。当传入的User对象的gender对象不为空,则会进行拼接,否则不会,执行到1=1就结束了

where标签

在上面的例子中,为了不报错需要书写1=1这种特殊处理,where标签能够更智能的处理这种情况

  1. where标签下的if标签,如果条件不通过,则不会生成子句
  2. 会自动去掉某些条件前多余的and或者or,参考上面if标签的使用中and gender = #{gender}中的and

利用where标签改造一下

    <select id="testWhereLabel" parameterType="user">
        select * from t_user
        <where>
            <if test="gender != null">
                and gender = #{gender}
            </if>
        </where>
    </select>

此时若过gendernull,最终的sqlselect * from t_user,反之则是select * from t_user where gender = #{gender},这里的and会被智能的去掉,如果有多个子标签,则and会被保留

trim标签

个人用的不太多,主要作用是可以在语句前后做增加和删除

  • prefix:添加前缀
  • suffix:添加后缀
  • prefixOverrides:删除前缀
  • suffixOverrides:删除后缀

简单的演示下使用

    <select id="testTrimLabel" parameterType="user">
        select * from t_user
        <trim prefix="where" suffixOverrides=",">
            <if test="gender != null">
                gender = #{gender},
            </if>
        </trim>
    </select>

if标签中的条件不满足时,也会智能不处理,若满足条件,则在包括的语句前添加prefix,并且删除掉语句末尾的suffixOverrides

set标签

一般用于update语句中,用起来其实和where标签很类似

<update id="testSetLabel" parameterType="cn.mgl.pojo.User">
  update t_user
  <set>
    <if test="username != null and username != ''">
      username = #{username},
    </if>
    <if test="gender != null and gender != ''">
      gender = #{gender},
    </if>
  </set>
</update>

上面的语句中,实现的需求就是当usernamegender不是null或者空字符串时,才会拼接set语句,并且会去掉子句后面的。这里有个问题就是当二者都不满足条件,就会产出一条错误的sql语句,更推荐用if+手写sql的方式来保证sql语句的正确性

choese when otherwise

这个其实就相当于if...else if...else的语法,都是配合着使用,还是以查询为例子,可以提供username,age的查询条件,当提供了username则使用username,如果没有提供username却提供了age,则使用age,否则则使用默认的查询条件

<select id="testChooseLabel" parameterType="cn.mgl.pojo.User">
  select * from t_user
  <where>
    <choose>
      <when test="username != null and username != ''">
        username = #{username}
      </when>
      <when test="gender != null and gender != ''">
        gender = #{gender}
      </when>
      <otherwise>
        age &lt; 10
      </otherwise>
    </choose>
  </where>
</select>
foreach标签

可用于循环遍历数组或集合,一般常用与批量增删改操作,有以下属性可以使用

  • collection:需要被循环的值
  • item:被循环的每个元素可用别名
  • index:循环的索引
  • open:在子句生成的开头插入指定字符
  • close:在子句生成的结尾插入指定字符
  • separator:循环之间的分隔符

以删除语句为例子,这里提供了两种写法,都可以实现批量删除

    <delete id="testForeachOfDel">
        delete from t_user
        where
        <foreach collection="ids" item="id" separator=",">
            id = #{id}
        </foreach>
    </delete>

    <delete id="testForeachOfDel2">
        delete from t_user
        where id in (
        <foreach collection="ids" item="id" separator=",">
            #{id}
        </foreach>
        )
    </delete>

需要注意的是,这里collection使用了ids,是因为对应接口定义时参数使用了@Param("ids") List<Long> ids,否则运行后会报错,并告知你应该使用arg0, collection, list这些底层映射的默认可用值,显然这些可读性不好,更应该指定mapkey,目前的设置,可用的就变为了idsparam1

像批量插入和修改都是同理的,只要是合法的sql语句就能利用foreach标签批量构建出预期值

sql标签和include标签

前者用来定义sql片段,后者用来引入到某个sql中,用批量删除的语句来演示

    <delete id="testForeachOfDel2">
        <include refid="delSlice"></include>
        where id in (
        <foreach collection="ids" item="id" separator=",">
            #{id}
        </foreach>
        )
    </delete>
    <sql id="delSlice">
        delete
        from t_user
    </sql>

用法比较简单,可以根据需求来决定提取哪些是可以复用的语句,比如繁琐的字段名就很适合定义为片段

Mybatis的高级映射

假设当前新增了一个Dept类存储部门信息,且每个用户都有对应的部门,此时就形成了多对一的关系,那么User对象当中就应该多一个Dept类型的成员属性

先定义一个部门类

package cn.mgl.pojo;

public class Dept {
    private Long dept_id;
    private String dept_name;

    public Long getDept_id() {
        return dept_id;
    }

    public void setDept_id(Long dept_id) {
        this.dept_id = dept_id;
    }

    public String getDept_name() {
        return dept_name;
    }

    public void setDept_name(String dept_name) {
        this.dept_name = dept_name;
    }
}

在数据库中也要创建对应的表结构

CREATE TABLE `t_dept` (
                          `dept_id` int DEFAULT NULL,
                          `dept_name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

除此之外还需要对原本User类添加一个成员属性dept,类型为Dept,并添加对应的getter,setter函数

(相关的数据库数据自行添加)

多对一映射的方式:
  • 一条外链接Sql映射结果
    <resultMap id="userMap" type="user">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <result property="dept.dept_id" column="dept_id"></result>
        <result property="dept.dept_name" column="dept_name"></result>
    </resultMap>
    <select id="testMapping" resultMap="userMap">
        select a.username,
               a.age,
               a.gender,
               a.created_at,
               a.updated_at,
               a.id,
               b.dept_id,
               b.dept_name
        from t_user a
                 left join t_dept b on a.dept_id = b.dept_id
    </select>

这种用法的select标签不使用resultType而是resultMap,注意对应的写法:<result property="dept.dept_id" column="dept_id"></result>中的deptUser类中的属性名,不可以随便写,这里可以不需要把所有映射都写上,如果确定类属性和结果集列名匹配得上可以不写

执行相关语句后,查询用户时也能把部门信息查出来并映射到成员嵌套对象中

一文搞懂MyBatis的使用

  • 使用映射中的关联标签
    <resultMap id="userMap" type="user">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <association property="dept" javaType="dept">
            <id property="deptId" column="dept_id"></id>
            <result property="deptName" column="dept_name"></result>
        </association>
<!--        <result property="dept.dept_id" column="dept_id"></result>-->
<!--        <result property="dept.dept_name" column="dept_name"></result>-->
    </resultMap>

只需要把原本的手动级联映射改为association标签,设置javaTypeDept类,接着配置类属性和字段名映射关系,实现效果和方式一完全一致

  • 分步查询

所谓的分布查询就是将查询分为步骤一、步骤二...以此类推,比如这个实例中,可以先查询出用户表信息,这是第一步,由于用户表存储了部门id,所以可以通过查询出来的部门id再去查询相关部门信息,这是第二步

新增一个DeptDao的接口,目前只写一个抽象方法

package cn.mgl.dao;

import cn.mgl.pojo.Dept;

public interface DeptDao {
    Dept getById(Long id);
}

接着添加对应的deptMapper.xml,编写一条根据id查询信息的sql(需要在mybatis-config.xml添加mapper

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.mgl.dao.DeptDao">
  <select id="getById" resultType="dept">
    select dept_id, dept_name
    from t_dept
    where dept_id = #{deptId}
  </select>
</mapper>

改写原本UserMapper.xml编写的查询,不需要外链接,只写简单的单表查询,并且association中添加一个select属性,指定为namespace+id的格式,column属性则为子语句的查询条件

    <resultMap id="userMap" type="user">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <association property="dept" select="cn.mgl.dao.DeptDao.getById" column="dept_id"></association>

        <!--        <association property="dept" javaType="dept">-->
        <!--            <id property="deptId" column="dept_id"></id>-->
        <!--            <result property="deptName" column="dept_name"></result>-->
        <!--        </association>-->

        <!--        <result property="dept.dept_id" column="dept_id"></result>-->
        <!--        <result property="dept.dept_name" column="dept_name"></result>-->
    </resultMap>
		<select id="testMapping" resultMap="userMap">
        select username,
               age,
               gender,
               created_at,
               updated_at,
               id
        from t_user
    </select>

执行查询后,可以看到执行了两条语句,并且会自动将第一步查询出来的dept_id当作查询参数传入第二条sql

一文搞懂MyBatis的使用

这样做的好处:

  1. 代码的复用性增强,耦合度降低
  2. 支持延迟加载,访问不到的数据可以先不查询,增加查询效率
看完多对一,接下来看看一对多
  • 利用collection标签

其实区别只是主体不同,现在要改成查询部门下的全部用户,则主体变更为部门,那么此时部门的类中应该有一个用户类型的集合才存储该部门下的所有用户(下面省略Java代码编写,自行操作)

    <resultMap id="clazzCollect" type="clazz">
        <id property="clazzId" column="dept_id"></id>
        <result property="clazzName" column="dept_name"></result>
        <collection property="collection" ofType="user">
            <id property="id" column="id"></id>
            <result property="username" column="username"></result>
        </collection>
    </resultMap>
    <select id="testGetByCollection" resultMap="clazzCollect">
        select a.dept_id, a.dept_name, b.id, b.dept_id, b.username, b.gender, b.age
        from t_dept a
                 left join t_user b on a.dept_id = b.dept_id
    </select>

resultMap中,有一个collection子标签,其中的property写了collection是因为在User类中有成员属性名就是collection,后面的ofType填写这个集合中类型,子标签的用法就大同小异了

执行查询后,虽然sql结果集为4条,但会将最终映射出来的数据只有3条(同一个部门归类),且Dept类中的集合成功赋值

一文搞懂MyBatis的使用

  • 和一对多类似的分步查询

这个原理就一样了,先做第一步查出部门,再做第二步查出部门下所有用户

    <resultMap id="clazzCollect" type="clazz">
        <id property="clazzId" column="dept_id"></id>
        <result property="clazzName" column="dept_name"></result>
        <collection property="collection" select="cn.mgl.dao.UserDao.selectByDeptId" column="dept_id">
        </collection>
    </resultMap>
    <select id="testGetByCollection" resultMap="clazzCollect">
        select dept_id, dept_name
        from t_dept
    </select>

与第一种方式的区别是,原本的连表查询改为单表查询,并且collection标签中新增select属性,对应的查询方法自行添加,实现的最终效果也完全一致

延迟加载

所谓的延迟加载就是没用到的时候不执行对应的sql,用到再执行,以减少查询的方式增加性能

一对多与多对一中的延迟加载机制是一样的

  1. 对某个查询单独使用fetchType="lazy",用上面的例子演示
    <resultMap id="clazzCollect" type="clazz">
        <id property="clazzId" column="dept_id"></id>
        <result property="clazzName" column="dept_name"></result>
        <collection fetchType="lazy" property="collection" select="cn.mgl.dao.UserDao.selectByDeptId" column="dept_id">
        </collection>
    </resultMap>
    <select id="testGetByCollection" resultMap="clazzCollect">
        select dept_id, dept_name
        from t_dept
    </select>
  1. 在全局配置中,添加一个setting标签,设置lazyLoadingEnabled="true",此时全部sql都会走延时加载
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"></setting>
        <setting name="logImpl" value="STDOUT_LOGGING"></setting>
        <setting name="lazyLoadingEnabled" value="true"></setting>
    </settings>

若在开启全局配置后,就是想要某个sql不走延时加载,只需要添加fetchType="eager"即可

一般推荐全局开启,减少查询增加性能

了解Mybatis的缓存机制

如这次执行查询后,将查询结果放到内存中,如果下次还是这条查询语句,则直接从缓存中取,不需要再去数据库中查询,以此来提高效率性能,但是只对select语句有效

一级缓存

针对的事sqlSession

下面的例子中:

  1. SqlSessionUtil.openSqlSession简单封装的获取sqlSession对象
  2. mapper.test是一条简单的selectSql
  3. mapper.update是一条简单的updateSql
        SqlSession sqlSession = SqlSessionUtil.openSqlSession();

        UserDao mapper = sqlSession.getMapper(UserDao.class);
        List<User> test = mapper.test();
        List<User> test2 = mapper.test();
        System.out.println(test2);
        System.out.println(test);

运行后,select只执行了一次,第二次再次查询直接从缓存中取,所以没有sql执行日志输出一文搞懂MyBatis的使用

下面演示的是一级缓存失效
  • 手动执行sqlSession的clearCache方法sqlSession.clearCache();,比较简单不演示
  • 在同一个SqlSession的情况下,两条select语句之间执行了update,insert,delete任一,一级缓存会被清空,代码用update举例子
        SqlSession sqlSession = SqlSessionUtil.openSqlSession();

        UserDao mapper = sqlSession.getMapper(UserDao.class);
        List<User> test = mapper.test();
        User user = new User();
        sqlSession.clearCache();
        user.setId(14L);
        mapper.updateUser(user);
        List<User> test2 = mapper.test();
        System.out.println(test2);
        System.out.println(test);

运行后可以看到,即使再次执行相同的select,也不会走缓存

一文搞懂MyBatis的使用

  • 不同SqlSession执行相同的select查询
        SqlSession sqlSession = SqlSessionUtil.openSqlSession();
        SqlSession sqlSession2 = SqlSessionUtil.openSqlSession();

        UserDao mapper = sqlSession.getMapper(UserDao.class);
        UserDao mapper2 = sqlSession2.getMapper(UserDao.class);

        List<User> test = mapper.test();
        List<User> test2 = mapper2.test();

        System.out.println(test2);
        System.out.println(test);

运行后可以看到执行了两次查询

一文搞懂MyBatis的使用

二级缓存

这个设置需要满足一定的条件才会开启,且是针对SqlSessionFactory的,前面说过可以根据不同的环境获取不同的SqlSessionFactory,如有两个SqlSession,即使他们都执行一样的sql语句,也不会走二级缓存

开启前提条件:

  1. mybatis-config.xml中,settings下的标签卡其了全局缓存,默认就是true<setting name="cacheEnabled" value="true">,相当于不配置就是开启
  2. 在需要使用缓存的SqlMapper.xml中添加一个<cache/>标签
  3. 被缓存的pojo类必须实现Serializable接口,让其拥有可序列化特性
  4. sqlSession对象关闭或提交之后,一级缓存中的数据才会被写入二级缓存中,此时缓存才能体现

前三步比较简单,直接看第四步的代码

        SqlSession sqlSession = SqlSessionUtil.openSqlSession();

        UserDao mapper = sqlSession.getMapper(UserDao.class);
        List<User> test = mapper.test();
        System.out.println(test);
        sqlSession.close();

        SqlSession sqlSession2 = SqlSessionUtil.openSqlSession();
        UserDao mapper2 = sqlSession2.getMapper(UserDao.class);
        List<User> test2 = mapper2.test();
        System.out.println(test2);

运行后的输出,有一句Cache Hit Ratio且没有第二次sql日志输出,说明缓存命中了

一文搞懂MyBatis的使用

二级缓存失效情况
  • 和一级缓存一样,中间执行过select,update,insert语句时,缓存也会失效
  • 不同的sqlSessionFactory对象

想测试的话比较简单,便不再演示

查询分页

Mybatis使用PageHelper插件

这个插件是帮助处理分页需求的,正常情况下分页查询需要接上limit startIndex,pageSize进行查询,通过这个插件可以让开发过程减少一些重复冗余的分页编写

通过maven添加依赖

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper</artifactId>
  <version>5.3.2</version>
</dependency>

接着在mybatis-config.xml中添加插件配置,注意顺序问题

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
    </plugins>

sql中不需要添加分页语句,只需要在查询语句执行前,调用一次startPage方法并传入相关参数,简单讲讲传参的计算,假设以10条数据为一页,查询第2页的数据,则需要传入2,10,后续的计算中则为(2-1)*10,10,最终结果就是limit 10,10

public static void main(String[] args) {
    SqlSession sqlSession = SqlSessionUtil.openSqlSession();
    UserDao mapper = sqlSession.getMapper(UserDao.class);
    PageHelper.startPage(2,10);
    List<User> users = mapper.selectAll();
    users.forEach(System.out::println);
    PageInfo<User> tPageInfo = new PageInfo<>(users);
    sqlSession.close();
}

一文搞懂MyBatis的使用 简单的说下原理,执行了插件方法startPage后,会缓存分页数据到ThreadLoacl当中,只会对执行了startPage方法后的第一个查询语句起效

如果不用分页插件也可以实现,先查询出表的总条数,再按照分页规则自行处理sql即可,插件只是帮忙处理了这几个步骤

注解式开发

mybatis除了xml以外,还提供了注解式开发,不过这种方式并不推荐使用,因为不好维护,除非是非常简单的sql语句,能用xml尽量用,不过也可以了解下使用方式,以基础的增删改查为例子,分别使用对应的注解,并传入sql的字符串,效果与xml文件配置是相同的

public interface UserAnnoDao {
    @Insert("insert into t_user (username, password, age, gender) values (#{username}, #{password}, #{age}, #{gender})")
    int insertUser(User user);

    @Delete("delete from t_user where id = #{id}}")
    int deleteUser(Long id);

    @Update("update t_user set username = #{username} where id = #{id}")
    int updateUser(User suer);

    @Select("select * from t_user where id #{id}}")
    User selectById(Long id);
}

完:)

转载自:https://juejin.cn/post/7178680539514142779
评论
请登录