Mybatis

Java笔记——Mybatis

🛴我的第一个Mybatis程序

1、创建数据库

image-20210720211142864

这是之前我做设计模式课设中建的一张表,下面我们就来查询这这张表中所有的记录,为了方便,就只查下前3个属性吧。

2、创建maven项目

​ 删除src目录;

​ 导入依赖:mysql、mybatis、junit

3、创建子模块module

同样选择maven项目,这样后面的再次创建maven项目时可以直接从父项目继承配置项。

4、编写mybatis核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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>
<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?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="com/twg/dao/UserDao.xml"/>
</mappers>
</configuration>

5、编写mybatis工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MybatisUtils {

private static SqlSessionFactory sqlSessionFactory;

static {
try {
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}

public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}

}

6、编写代码

  • 实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class User {
    private int id;
    private String username;
    private String password;

    public User() {
    }

    public User(int id, String username, String password) {
    this.id = id;
    this.username = username;
    this.password = password;
    }
    }

  • DAO

    1
    2
    3
    4
    5
    6
    7
    8
    package com.twg.dao;

    import com.twg.entity.User;
    import java.util.List;

    public interface UserDao {
    List<User> getUserList();
    }
  • DAO持久层映射文件,由原来的UserDaoImpl类转变成一个Mapper配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?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="com.twg.dao.UserDao">
    <select id="getUserList" resultType="com.twg.entity.User">
    select * from test.t_user
    </select>
    </mapper>

    特别注意:

    namespace:指定(绑定)了DAO接口的位置;

    id:为namespace所绑定的接口中的方法名;

    resultType:指定了id所绑定的方法的返回值类型,如果是返回一个集合,则这里写集合中元素的类型;

7、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.twg.dao;

import com.twg.entity.User;
import com.twg.utils.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

public class UserDaoTest {

@Test
public void test() {
SqlSession sqlSession = MybatisUtils.getSqlSession();

// 方法1:getMapper
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> userList = userDao.getUserList();
// 方法2:
//List<User> userList = sqlSession.selectList("com.twg.dao.UserDao.getUserList");

for (User user : userList) {
System.out.println(user);
}

sqlSession.close();

}
}

8、可能会遇到的问题

  • 映射文件没有在mybatis主配置文件中注册

    image-20210720205949405

  • 构建项目后,找不到映射文件

    • 在porn.xml中配置resources,来防止资源导出异常;
    • 也可以直接将映射文件放到resources目录下;
  • 映射文件中的参数一定要写对

    • namespace:表明这个映射文件所绑定的接口类是哪个
    • id:绑定接口中的哪个方法,方法名一定要和接口中定义的名字一样
    • resultType:表明返回值类型

    image-20210720210326930

🛫解锁其它姿势——CRUD

  1. 回顾下通过mybatis写一个接口的流程

    1. 在Mapper中定义一个抽象方法

    2. 在映射文件(XXMapper.xml)中绑定这个方法

    3. 进行测试/使用,注意如果是增删改需要提交事物(”sqlSession.commit();”)

  2. 看看在映射文件中还可以写哪些标签

    image-20210721221129218

  3. 1
    2
    3
    4
    5
    6
    /**
    * 向用户表中插入一条记录
    * @param user:
    * @return 新增加的记录的id
    */
    int inserUser(User user);
    1
    2
    3
    4
    <insert id="inserUser" parameterType="com.twg.entity.User">
    insert into test.t_user (username, password)
    values (#{username}, #{password})
    </insert>
  4. 1
    2
    3
    4
    5
    6
    /**
    * 通过指定id删除一条用户表记录
    * @param id
    * @return
    */
    int delUser(int id);
    1
    2
    3
    4
    5
    <delete id="delUser">
    delete
    from t_user
    where id = #{id};
    </delete>
  5. 1
    2
    3
    4
    5
    6
    /**
    * 更新一条用户表信息
    * @param user
    * @return
    */
    int updateUser(User user);
    1
    2
    3
    4
    5
    6
    <update id="updateUser" parameterType="com.twg.entity.User">
    update t_user
    set username = #{username},
    password=#{password}
    where id = #{id};
    </update>
  6. 1
    2
    3
    4
    5
    6
    /**
    * 根据ID查询用户
    * @param id:用户的id
    * @return 返回指定用户id的信息
    */
    User getUserById(int id);
    1
    2
    3
    4
    5
    <select id="getUserById" resultType="com.twg.entity.User">
    select *
    from test.t_user
    where id = #{id}
    </select>

🎁万能的Map

当实体类的属性过多时(表的字段太多),可以考虑使用Map。

1
2
3
4
5
6
/**
* 通过map的方式查找指点用户
* @param map
* @return
*/
User getUserById2(Map<String, Object> map);
1
2
3
4
5
<select id="getUserById2" parameterType="map" resultType="com.twg.entity.User">
select *
from test.t_user
where id = #{helloId}
</select>
1
2
3
4
5
6
7
8
9
10
11
@Test
public void testGetUserById2() {
SqlSession sqlSession = MybatisUtils.getSqlSession();

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("helloId",1);
System.out.println(mapper.getUserById2(map));

sqlSession.close();
}

🎭模糊查询

为了防止SQL注入,注意在传入参数时将参数写si.

1
2
//方法1
List<User> userList = mapper.getUserList("%李%");
1
2
//方法2
select * from user where name like "%"#{value}"%"

🍀环境配置

环境配置

Mybatis可以配置多个环境,但是每个SqlSessionFactory只能选择一种。

默认事物管理器——JDBC

连接池——POOLED

image-20210725203805540

属性

可以通过properties属性来应用配置文件。

这些属性都是可以替换了,例如.properties文件

image-20210725205432826

image-20210725205256245

可以通过引入外部配置文件来确定参数,也可以通过标签来指定。外部文件的优先级更高。

image-20210725205507441

配置好参数,就可以通过 ${ } 的方式使用了。

类型别名

为什么要取别名?——为了简洁

下面这些返回值类型、参数类型写的太长了,也容易出错。

image-20210725205938230

2种解决办法:

  • 给实体类取别名
  • 给实体类的包取别名,这里自动自动将别名取成,这个包中类名,并将首字母小写。如果需要改成别的名字,在实体类上加上注解@Alias(“xxx”)

image-20210725210404834

映射器

绑定映射文件。

  • 通过指定资源目录——推荐✨
1
2
3
<mappers>
<mapper resource="com/twg/dao/UserMapper.xml"/>
</mappers>
  • 下面两种有限制:
    • 接口和其配置文件需要在同一包下
    • 需要同名
1
2
3
<mappers>
<package name="com.twg.dao.UserMapper"/>
</mappers>
1
2
3
<mappers>
<mapper class="com.twg.dao.UserMapper"/>
</mappers>

生命周期和作用域

image-20210725212908950

image-20210725213039250

🎗解决属性名与字段名不一致的问题

问题:当属性名与字段名不一致时,查询出来的结果会显示“null”

例如,在表中密码字段是pwd,但是我在实体类中的属性写的是password.

解决:

  • 起别名

    1
    2
    3
    4
    5
    <select id="getUserById" resultType="com.twg.entity.User">
    select id,name,pwd as password
    from test.t_user
    where id = #{id}
    </select>
  • resultMap

    结果集映射

📃日志

当代码出现异常,需要排错时,日志就是最好的工具!

以前:手动输出,打印

现在:日志工厂

1
2
3
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

image-20210725220515609

Log4j

也是一种日志,是Apache的一个开源项目,通过他可以控制日志的输出格式,日志的输送目的地、日志的级别等。

1、导入log4j依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

2、添加log4j.properties配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file

#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=【%c】-%m%n

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

3、配置mybatis

1
2
3
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>

image-20210726202742355

📑分页

为什么需要分页——数据太多,不方便处理

之前:通过SQL中的limit

1
2
select * from user limit 3
// 查询[0,3)

现在:mybatis实现

⚙使用注解

1
2
3
4
5
package org.mybatis.example;
public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
}
image-20210726211745259

同样,使用注解这种方式,也需要在配置文件中注册mapper。

当存在多个参数时,每个参数前面必须加上@param()注解

  • 基本数据类型、String需要加上
  • 应用类型不需要
  • 只有一个参数可以不用
  • 在sql中引用的就是@param中填写的参数
1
2
3
4
public interface UserMapper {
@Select("SELECT * FROM user where id = #{id} and name = #{name}")
User getUserById(@param("id") int id, @param("name") String username);
}

🎈Lombok

使用Lombok,可以不必再写哪些死板的getter、setter方法了。

但是:舍弃了一定规范,得到了一些便利。

  1. idea安装Lombok插件

    image-20210726214816522

  2. 导入依赖

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>RELEASE</version>
    <scope>compile</scope>
    </dependency>
  3. 使用注解@Data

    image-20210726215304305

🏐动态SQL

what:根据不同的条件生成不同的SQL语句

在实际的应用中常常通过指定多个条件来查询,例如,这里有一张购买记录表,我需要可以指定VIP的id(v_id),产品的id(p_id)以及购买时间来查询,当用户没有指定某个条件时,查询时就不对这个字段进行限制,这里就可以通过动态SQL来实现。

image-20210727162040219

官网中给了这样一些标签

image-20210727163240019

if

  1. 编写接口

    接口中有一个Map参数,是为了指定查询条件。

    1
    2
    3
    4
    5
    6
    7
    8
    public interface BuyProductMapper {
    /**通过多个条件,查询BuyProduct表
    * @param map
    * @return
    */
    public List<BuyProduct> getBuyIf(Map map);
    }

  2. 编写mapper

    为了实现动态的SQL,这里主要是使用了标签,当我们传的map参数中,存在某个key,就在原先的SQL语句后面拼接查询条件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?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="com.twg.dao.BuyProductMapper">
    <select id="getBuyIf" parameterType="map" resultType="BuyProduct">
    select * from test.buyproduct where 1=1
    <if test="v_id != null">
    and v_id = #{v_id}
    </if>
    <if test="p_id != null">
    and p_id = #{p_id}
    </if>
    <if test="time != null">
    and time > #{time}
    </if>
    </select>
    </mapper>
  3. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BuyProductMapperTest {
@Test
public void getBuyIf(){
SqlSession sqlSession = MybatisUtils.getSqlSession();

BuyProductMapper mapper = sqlSession.getMapper(BuyProductMapper.class);
HashMap<String, String> map = new HashMap<>();
// map.put("v_id","1");
// map.put("p_id","1");
map.put("time","2021-7-1");
List<BuyProduct> buys = mapper.getBuyIf(map);
for (BuyProduct buy : buys) {
System.out.println(buy);
}

sqlSession.close();

}
}
  • map:{“time”,”2021-7-1”}

    可以看到查询到了所有时间在2021-7-1之后的记录

    image-20210727162919531

  • map:{(“v_id”,”1”), (“p_id”,”1”)}

    通过指定用户id和产品id,我们查询到了一条数据

    image-20210727163057263

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

  1. 接口

    1
    2
    3
    4
    5
    6
    /**
    * 从多个条件中选取一个来查询购物记录
    * @param map
    * @return
    */
    public List<BuyProduct> getBuyChoose(Map map);
  2. mapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <select id="getBuyChoose" parameterType="map" resultType="BuyProduct">
    select * from test.buyproduct
    <where>
    <choose>
    <when test="v_id">
    and v_id like #{v_id}
    </when>
    <when test="p_id">
    and p_id like #{p_id}
    </when>
    <otherwise>
    and time >= CURDATE()
    </otherwise>
    </choose>
    </where>
    </select>

    这里我使用了等价于下面的

    1
    2
    3
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
    ...
    </trim>
  3. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void getBuyChoose(){
SqlSession sqlSession = MybatisUtils.getSqlSession();

BuyProductMapper mapper = sqlSession.getMapper(BuyProductMapper.class);
HashMap<String, String> map = new HashMap<>();
// map.put("v_id","1");
// map.put("p_id","1");
List<BuyProduct> buys = mapper.getBuyChoose(map);
for (BuyProduct buy : buys) {
System.out.println(buy);
}
sqlSession.close();
}
  • 当map为空时,根据配置文件中写的sql,他会默认添加中的条件,也就是默认查询到今天的购买记录。

image-20210727174434406

  • map:{(“v_id”,”1”), (“p_id”,”1”)}

    由于choose、when、otherwise类似于Java中的switch语句,哪怕我这里给出了两个查询条件,它根据我们写的mapper,它会优先取v_id作为条件,所以查询出了下面这些记录。

    image-20210727174807964

trim、where、set

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

1
2
3
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>

set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。

来看看与 set 元素等价的自定义 trim 元素吧:

1
2
3
<trim prefix="SET" suffixOverrides=",">
...
</trim>

foreach

1
2
3
4
5
6
7
8
9
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

foreach元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。

🌊缓存

将数据缓存到内存中,提高访问速度。

  • 一级缓存(本地缓存)——默认,sqlSession域
  • 二级缓存——namespace
  • 自定义缓存

一级缓存

与数据库同一次会话期间查询到的数据会放在本地缓存中;

之后再次查询时会直接从缓存中获取;

1
2
3
4
5
6
7
8
9
10
@Test
public void testGetUserById() {
SqlSession sqlSession = MybatisUtils.getSqlSession();

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.getUserById(1));
System.out.println("-----------------------");
System.out.println(mapper.getUserById(1));
sqlSession.close();
}

image-20210728220543416

从日志中,我们可以看到,虽然查询了两次,但是只有一条select语句。

失效的情况:

  • 查询不同的东西
  • 增删改操作,可能会改变数据,缓存也会失效
  • 查询不同的mapper

二级缓存

因为一级缓存的作用域太低了,所以出现了二级缓存。——namespace域,全局缓存。

工作机制

  • 一个会话查询一条数据,这个数据就会放到一级缓存
  • 如果当前会话关闭了,这个会话对应的一级缓存就没了;此时一级缓存的数据就会保存到二级缓存中
  • (在同一个mapper)新的会话再次查询信息时,就会直接从二级缓存中获取数据
  • 不同的mapper查询出的数据会放到自己对应的缓存中

步骤:

  1. 显式地在配置文件中,开启全局缓存

    image-20210729092625651

  2. 在当前mapper中使用二级缓存

    默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

    1
    <cache/>

    也可以定制其中的参数:

    1
    2
    3
    4
    5
    <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void testGetUserById() {
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    System.out.println(mapper.getUserById(1));
    sqlSession.close();

    SqlSession sqlSession2 = MybatisUtils.getSqlSession();
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    System.out.println("-----------------------");
    System.out.println(mapper2.getUserById(1));
    sqlSession2.close();
    }

    image-20210729093139833

    上面的代码中,我创建了两个sqlsession对象,并通过该对象获取中同类型的mapper,当我使用完第一个sqlsession并关闭后,再使用一个新的sqlsession时,可以看到日志中并没有再次查询数据库,说明二级缓存起作用了。

    自定义缓存Ehcache

    除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

  4. 导入依赖

    1
    2
    3
    4
    5
    6
    7
    <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
    <dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.1</version>
    </dependency>

  5. 启用缓存

    1
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
  6. 添加ehcache配置文件——ehcache.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
     <?xml version=1.0 encoding=UTF-8 ?>
    <ehcache xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
    xsi:noNamespaceSchemaLocation=http://ehcache.org/ehcache.xsd
    updateCheck=false>

    <diskStore path=./tmpdir/Tmp_EhCache/>

    <defaultCache
    eternal=false
    maxElementsInMemory=10000
    overflowToDisk=false
    diskPersistent=false
    timeToIdleSeconds=1800
    timeToLiveSeconds=259200
    memoryStoreEvictionPolicy=LRU/>

    <cache
    name=cloud_user
    eternal=false
    maxElementsInMemory=5000
    overflowToDisk=false
    diskPersistent=false
    timeToIdleSeconds=1800
    timeToLiveSeconds=1800
    memoryStoreEvictionPolicy=LRU/>
    </ehcache>

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!