MyBatis,持久层的框架。
持久层,负责将数据到保存到数据库的那一层。
框架:一套可重用的基础代码模型。
开始
我们讨论两种方式:
限定名
Mapper代理
在其他资料中,可能没有"限定名"这个名称,这个是我取的。
限定名的方式,例如:1 List<User> users = sqlSession.selectList("whatever.selectAll" );
在查询方法中传入一个指向查询映射的字符串。 Mapper代理的方式,例如:1 2 UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; List<User> users = userMapper.selectAll();
限定名
添加依赖
在pom.xml
文件中添加依赖的坐标,需要两个依赖:
mysql-connector-java
mybatis
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 <dependencies > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.29</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.10</version > </dependency > </dependencies >
配置文件
在src/main/resources
目录下创建mybatis-config.xml
,用于MyBatis的全局配置信息(包括数据库连接)。
一般命名为mybatis-config.xml
,但这只是习惯,并非强制要求。
mybatis-config.xml
的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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.cj.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://127.0.0.1:3306/test_mybatis" /> <property name ="username" value ="root" /> <property name ="password" value ="MySQL@2022" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="mapper/UserMapper.xml" /> </mappers > </configuration >
映射文件
在上文的配置文件中,我们在<mappers>
标签的子标签<mapper>
,定义了文件的路径,我们新建该文件,内容如下:
1 2 3 4 5 6 7 <?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 ="whatever" > <select id ="selectAll" resultType ="com.kakawanyifan.pojo.User" > select * from user; </select > </mapper >
注意:在这里<mapper>
标签的namespace
属性的值,我们随便填了一个whatever
。
SQL映射文件可能有警告,如下
原因是:IDEA和数据库没有建立连接,不能识别表信息。但,并不影响程序的执行。 解决方法是:点击左侧Database
,添加数据库连接。
测试
上述工作都完成后,我们试一下。
假设存在一张表user
,表结构如下:
1 2 3 4 5 6 7 8 CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar (20 ) DEFAULT NULL , `password` varchar (20 ) DEFAULT NULL , `gender` char (1 ) DEFAULT NULL , `addr` varchar (30 ) DEFAULT NULL , PRIMARY KEY (`id` ) ) ENGINE =InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET =utf8mb4 COLLATE =utf8mb4_0900_ai_ci
新建一个类User
,在com.kakawanyifan.pojo
包下,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 package com.kakawanyifan.pojo;public class User { private int id; private String username; private String password; private String gender; private String addr; 【Getter和Setter代码略】 }
新建MyBatisDemo
,示例代码:
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 package com.kakawanyifan;import com.kakawanyifan.pojo.User;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;import java.util.List;public class MyBatisDemo { public static void main (String[] args) throws IOException { String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); List<User> users = sqlSession.selectList("whatever.selectAll" ); System.out.println(users); sqlSession.close(); } }
运行结果:
1 [com.kakawanyifan.pojo.User@1e178745, com.kakawanyifan.pojo.User@192c3f1e, com.kakawanyifan.pojo.User@26b3fd41]
解释说明:
SqlSession
:Java程序和数据库之间的会话。
SqlSessionFactory
:是"生产"SqlSession的"工厂"。
工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个"工厂类"中,以后都使用这个工厂类来"生产"我们需要的对象
通过sqlSessionFactory.openSession()
,需要手动提交事务,sqlSession.commmit()
。
如果要自动提交事务,可以使用sqlSessionFactory.openSession(true)
,传入一个Boolean类型的参数true
。
有部分资料,在此处没有sqlSession.close()
的代码。 在单独使用MyBatis时,sqlSeesion
使用完毕后是需要进行手动关闭的,但如果整合了Spring,就不需要了。
Mapper代理
在上文,存在硬编码的问题,在sqlSession.selectList()
方法中,还是写了"whatever.selectAll"
。
更好的方式是Mapper代理方式,
Mapper接口
在com.kakawanyifan.mapper
包下创建UserMapper
接口,示例代码:
1 2 3 4 5 6 7 8 9 package com.kakawanyifan.mapper;import com.kakawanyifan.pojo.User;import java.util.List;public interface UserMapper { List<User> selectAll () ; }
在Mapper接口中定义方法,方法名与映射文件中SQL语句的ID保持一致,参数类型以及返回值类型也要一致。
例如:映射文件中,id="selectAll"
,返回类型是resultType="com.kakawanyifan.pojo.User"
;对应接口UserMapper
的方法List<User> selectAll()
。
修改映射的名称空间
修改UserMapper.xml
的<mapper>
标签的namespace
为对应接口UserMapper
的全限定名。
1 <mapper namespace ="com.kakawanyifan.mapper.UserMapper" >
测试
修改MyBatisDemo
的如下代码
1 List<User> users = sqlSession.selectList("whatever.selectAll" );
改为
1 2 UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; List<User> users = userMapper.selectAll();
示例代码:
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 31 32 package com.kakawanyifan;import com.kakawanyifan.mapper.UserMapper;import com.kakawanyifan.pojo.User;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;import java.util.List;public class MyBatisDemo { public static void main (String[] args) throws IOException { String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; List<User> users = userMapper.selectAll(); System.out.println(users); sqlSession.close(); } }
运行结果:
1 [com.kakawanyifan.pojo.User@28975c28, com.kakawanyifan.pojo.User@3943a2be, com.kakawanyifan.pojo.User@343570b7]
在上文调用UserMapper
接口中的方法中:会根据UserMapper
的全类名匹配映射文件;通过调用的方法名匹配映射文件中的SQL标签。
进阶
打印SQL
打印SQL的方法其实很多,这里我们依赖log4j
。
添加log4j
的依赖,示例代码:
1 2 3 4 5 <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <version > 1.2.17</version > </dependency >
添加的是1版本的log4j。
在src/main/resources
目录下配置log4j
,文件名为log4j.xml
,该名称是固定的。内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j :configuration SYSTEM "log4j.dtd" > <log4j:configuration xmlns:log4j ="http://jakarta.apache.org/log4j/" > <appender name ="STDOUT" class ="org.apache.log4j.ConsoleAppender" > <param name ="Encoding" value ="UTF-8" /> <layout class ="org.apache.log4j.PatternLayout" > <param name ="ConversionPattern" value ="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n" /> </layout > </appender > <logger name ="java.sql" > <level value ="debug" /> </logger > <logger name ="org.apache.ibatis" > <level value ="info" /> </logger > <root > <level value ="debug" /> <appender-ref ref ="STDOUT" /> </root > </log4j:configuration >
然后我们在运行一次,运行结果如下:
1 2 3 4 DEBUG 06-12 13:02:11,025 ==> Preparing: select * from user; (BaseJdbcLogger.java:137) DEBUG 06-12 13:02:11,045 ==> Parameters: (BaseJdbcLogger.java:137) DEBUG 06-12 13:02:11,063 <== Total: 3 (BaseJdbcLogger.java:137) [com.kakawanyifan.pojo.User@446293d, com.kakawanyifan.pojo.User@69997e9d, com.kakawanyifan.pojo.User@793be5ca]
MyBatisX
上述,我们需要找一个Mapper接口对应的映射文件非常不方便。
IDEA中有一个插件MyBatisX
,安装这个插件后,我们点击"小鸟"图标,即可快速定位。
SqlSessionFactory工具类抽取
在上文,我们每一次创建连接,都有如下的操作
1 2 3 String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
我们可以创建一个工具类,以此避免SqlSessionFactory工厂类进行重复创建。
(就相当于每次买手机都需要重新创建一个手机生产工厂来给你制造一个手机一样,资源消耗非常大但性能却非常低。所以这么做是不允许的。)
优化思路是:
重复代码可以抽取工具类
对指定代码只需要执行一次可以使用静态代码块
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class SqlSessionFactoryUtils { private static SqlSessionFactory sqlSessionFactory; static { try { String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } public static SqlSessionFactory getSqlSessionFactory () { return sqlSessionFactory; } }
工具类抽取以后,以后在对Mybatis的SqlSession进行操作的时候,就可以直接使用
1 SqlSessionFactory sqlSessionFactory =SqlSessionFactoryUtils.getSqlSessionFactory();
这样就可以很好的解决上面所说的重复创建工厂导致性能低的问题了。
配置
MyBatis的配置文件结构如下:
configuration
(配置) :
properties
settings
typeAliases
typeHandlers
objectFactory
objectWrapperFactory
reflectorFactory
plugins
environments
databaseIdProvider
mappers
必须按照上述固定的顺序(可以有标签可以不写)。
我们讨论其中常用的标签。
完整的可以参考:https://mybatis.org/mybatis-3/zh/configuration.html
properties
可以利用<properties>
标签的resource
属性中指向的外部文件,也可以利用子标签<property>
。
例如,我们在src/main/resources
目录下,新建jdbc.properties
,内容如下:
1 2 jdbc.driver = com.mysql.cj.jdbc.Driver jdbc.url = jdbc:mysql://127.0.0.1:3306/test_mybatis
properties的配置文件如下:
1 2 3 4 <properties resource ="jdbc.properties" > <property name ="jdbc.username" value ="root" /> <property name ="jdbc.password" value ="MySQL@2022" /> </properties >
然后,我们就可以在下文配置数据库连接的时候用了,如下:
1 2 3 4 5 6 7 8 9 10 11 <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <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 >
settings
设置,例如:
1 2 3 4 5 6 <settings > <setting name ="mapUnderscoreToCamelCase" value ="true" /> <setting name ="lazyLoadingEnabled" value ="true" /> </settings >
解释说明:
mapUnderscoreToCamelCase
,是否开启驼峰命名自动映射,即从经典数据库列名A_COLUMN
映射到经典Java属性名aColumn
。
默认是关闭的
lazyLoadingEnabled
,延迟加载,这个我们会在下文讨论。
typeAliases
typeAliases,可为Java类型设置一个缩写名字。例如:
1 2 3 <typeAliases > <typeAlias type ="com.kakawanyifan.pojo.User" alias ="User" > </typeAlias > </typeAliases >
也可以不写alias
,则用类名作为别名(不区分大小写),例如:
1 2 3 <typeAliases > <typeAlias type ="com.kakawanyifan.pojo.User" > </typeAlias > </typeAliases >
但是不建议这么做,会导致无法通过映射文件定位到类。
还可以,以包为单位,设置改包下所有的类型都用类名作为别名(不区分大小写),例如:
1 2 3 <typeAliases > <package name ="com.kakawanyifan.pojo" /> </typeAliases >
注意,这里设置的只是别名,并不是说全限定名就不能用了。
对于常用的类型,已经预设了类型别名,如下:
别名
映射的类型
_byte
byte
_char
(since 3.5.10)
char
_long
long
_short
short
_int
、_integer
int
_double
double
_float
float
_boolean
boolean
string
String
byte
Byte
char
(since 3.5.10)
Character
long
Long
short
Short
int
、integer
Integer
double
Double
float
Float
boolean
Boolean
date
Date
decimal
、bigdecimal
BigDecimal
biginteger
BigInteger
environments
environments
<environments>
,多个连接数据库环境,有一个属性default
,用于设置默认使用的环境的id
。
environment
<environment>
,<environments>
的子标签,具体的连接数据库的环境信息,有一个属性id
,设置环境的唯一标识。
transactionManager
<transactionManager>
,<environment>
的子标签,事务管理方式,有一个属性tyep
,有两种取值:
type="JDBC"
:设置当前环境的事务管理都必须手动处理type="MANAGED"
:设置事务被管理,例如Spring中的AOP
dataSource
<dataSource>
,<environment>
的子标签,数据源,有一个属性type
,有三种取值:
type="POOLED"
:使用数据库连接池。type="UNPOOLED"
:不使用数据库连接池。type="JNDI"
,调用上下文的数据源,这种一般不用。主要使用的还是POOLED
。
<dataSource>
可以设置的属性标签<property>
的name
,常见的有:
driver
:数据库驱动类的全限定名。url
:数据库连接的URL地址。username
:登录数据库的用户名。password
:登录数据库的密码。poolMaximumActiveConnections
:最大活跃连接数,默认:10。poolMaximumIdleConnections
:最大空闲连接数。
例如:
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 31 32 33 34 35 36 37 38 39 40 <environments default ="mysql_test" > <environment id ="mysql_test" > <transactionManager type ="JDBC" /> <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 >
更多的参数可以参考:https://mybatis.org/mybatis-3/zh/configuration.html#environments
mappers
mapper
可以通过子标签<mapper>
引入映射文件,例如:
1 2 3 <mappers > <mapper resource ="mapper/UserMapper.xml" /> </mappers >
package
还可以,以包为单位,将包下所有的映射文件引入核心配置文件。
如果采用这种方式,需要注意的是:
Mapper接口名称和映射文件名称相同,并在同名目录下,如图:
1 2 3 <mappers > <package name ="com.kakawanyifan.mapper" /> </mappers >
在resources
中新建多层的目录,不能用.
分隔,要用/
分隔,否则IDEA会认为是建立一个文件夹,名称是com.kakawanyifan.mapper
小结
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 <?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 > <settings > <setting name ="mapUnderscoreToCamelCase" value ="true" /> <setting name ="lazyLoadingEnabled" value ="true" /> </settings > <typeAliases > <package name ="com.kaka.mybatis.bean" /> </typeAliases > <environments default ="mysql_test" > <environment id ="mysql_test" > <transactionManager type ="JDBC" /> <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 > <package name ="com.kakawanyifan.mapper" /> </mappers > </configuration >
映射
获取参数
两种方式
MyBatis获取参数,有两种方式:
#{}
:本质是占位符赋值,作用于数据库的服务端中。
${}
:本质是字符串拼接,作用于数据库的客户端(即我们写的程序)中。
单个字面量类型的参数
如果Mapper接口中的方法参数为单个的字面量类型,可以使用#{}
或${}
,并以任意的名称获取参数的值
示例代码:
1 User getUserByUsername (String username) ;
1 2 3 <select id ="getUserByUsername" resultType ="com.kakawanyifan.pojo.User" > select * from user where username = #{XingMing} </select >
多个字面量类型的参数
如果Mapper接口中的方法参数为多个时,此时MyBatis会自动将这些参数放在一个Map集合中,可以通过arg或者param两种方式获取:
通过arg,以arg0
、arg1
… 为键
通过param1,以param1
、param2
… 为键
(注意!arg是0开始的,param是1开始的)
示例代码:
1 2 3 User loginByArg (String username,String password) ;User loginByParam (String username,String password) ;
1 2 3 4 5 6 <select id ="loginByArg" resultType ="com.kakawanyifan.pojo.User" > select * from user where username = #{arg0} and password = #{arg1} </select > <select id ="loginByParam" resultType ="com.kakawanyifan.pojo.User" > select * from user where username = #{param1} and password = #{param2} </select >
Map集合类型的参数
如果参数过多,通过上述的方法就不一定方便了,此时可以手动创建Map集合,通过Map集合的键就可以获取相对应的值。
示例代码:
1 User loginByMap (Map<String,Object> map) ;
1 2 3 <select id ="loginByMap" resultType ="com.kakawanyifan.pojo.User" > select * from user where username = #{username} and password = #{password} </select >
使用@Param标识参数
可以通过@Param
注解标识Mapper接口中的方法参数,此时,会将这些参数放在map集合中,@Param
注解的值为键。
示例代码:
1 User loginByParamMap (@Param("username" ) String username,@Param ("password" ) String password) ;
1 2 3 <select id ="loginByParamMap" resultType ="com.kakawanyifan.pojo.User" > select * from user where username = #{username} and password = #{password} </select >
实体类类型的参数
如果Mapper接口中的方法参数为实体类对象,可以通过访问实体类对象中的属性名获取属性值
示例代码:
1 User loginByUserPojo (User user) ;
1 2 3 <select id ="loginByUserPojo" resultType ="com.kakawanyifan.pojo.User" > select * from user where username = #{username} and password = #{password} </select >
1 2 3 4 User user = new User(); user.setUsername("张三" ); user.setPassword("123" ); System.out.println(userMapper.loginByUserPojo(user));
运行结果:
1 User{id=1, username='张三', password='123', gender='男', addr='北京'}
增
“增”,依赖<insert>
标签,返回值为int
类型,表示被影响的行数。
示例代码:
1 int insertUser (User user) ;
1 2 3 <insert id ="insertUser" > insert into user values(null,#{username},#{password},#{gender},#{addr}) </insert >
1 2 3 4 5 6 User user = new User(); user.setUsername("张三三" ); user.setPassword("123456" ); user.setGender("男" ); user.setAddr("某地" ); System.out.println(userMapper.insertUser(user));
运行结果:
删
“删”,依赖<delete>
标签,返回值为int
类型,表示被影响的行数。
示例代码:
1 int deleteUserByUsername (String username) ;
1 2 3 <delete id ="deleteUserByUsername" > delete from user where username = #{username} </delete >
1 System.out.println(userMapper.deleteUserByUsername("张三三" ));
运行结果:
改
“改”,依赖<update>
标签,返回值为int
类型,表示被影响的行数。
示例代码:
1 int updatePasswordByUsername (@Param("password" ) String password,@Param ("username" ) String username) ;
1 2 3 <update id ="updatePasswordByUsername" > update user set password = #{password} where username = #{username}; </update >
1 System.out.println(userMapper.updatePasswordByUsername("333" ,"张三" ));
运行结果:
查
“查”,其实我们在上文都用过了,这里系统的讨论一下。
“查”,依赖<select>
标签,必须设置属性resultType
或resultMap
,用于设置实体类和数据库表的映射关系。
resultType
:自动映射,用于属性名和表中字段名一致的情况。
resultMap
:自定义映射,用于"字段名和属性名不一致"、"一对多"或"多对一"的情况。
查询一个实体类对象
例如,根据ID查询。
示例代码:
1 User getUserById (String id) ;
1 2 3 <select id ="getUserById" resultType ="com.kakawanyifan.pojo.User" > select * from user where id = #{id} </select >
特别注意,如果实际查询结果大于1 1 1 ,会报错TooManyResultsException
。
1 org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 9
查询一个List集合
我们本章的第一个例子,就是查询一个List集合。
示例代码:
1 2 3 <select id ="selectAll" resultType ="com.kakawanyifan.pojo.User" > select * from user; </select >
再举一个例子,示例代码:
1 List<Map<String, Object>> getUserToList(@Param ("username" ) String userName);
1 2 3 <select id ="getUserToList" resultType ="java.util.Map" > select * from user where username = #{username} </select >
1 System.out.println(userMapper.getUserToList("张三" ));
运行结果:
1 [{password=333, gender=男, id=1, addr=北京, username=张三}]
查询单个数据
例如,查询用户的总记录数。
示例代码:
1 2 3 <select id ="countUser" resultType ="int" > select COUNT(*) from user </select >
解释说明,resultType="int"
,这里用的是预设的别名。
查询单条数据为Map集合
示例代码:
1 Map<String, Object> getUserToMap (@Param("username" ) String userName) ;
1 2 3 <select id ="getUserToMap" resultType ="map" > select * from user where username = #{username} </select >
1 System.out.println(userMapper.getUserToMap("张三" ));
运行结果:
1 {password=333, gender=男, id=1, addr=北京, username=张三}
查询多条数据为Map集合
如果查询结果有多条呢?
示例代码:
1 System.out.println(userMapper.getUserToMap("张三三" ));
运行结果:
1 org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 12
解释说明:一条数据对应一个map,若有多条数据,就会产生多个Map集合,如果仍要以一个Map的方式返回数据,可以通过@MapKey
注解设置Map集合的键,值是每条数据的Map。
示例代码:
1 2 @MapKey ("id" )Map<String, Object> getUserToMap (@Param("username" ) String userName) ;
1 System.out.println(userMapper.getUserToMap("张三三" ));
运行结果:
1 {16={password=123456, gender=男, id=16, addr=某地, username=张三三}, 17={password=123456, gender=男, id=17, addr=某地, username=张三三}, 18={password=123456, gender=男, id=18, addr=某地, username=张三三}, 19={password=123456, gender=男, id=19, addr=某地, username=张三三}, 20={password=123456, gender=男, id=20, addr=某地, username=张三三}, 21={password=123456, gender=男, id=21, addr=某地, username=张三三}, 22={password=123456, gender=男, id=22, addr=某地, username=张三三}, 23={password=123456, gender=男, id=23, addr=某地, username=张三三}, 24={password=123456, gender=男, id=24, addr=某地, username=张三三}, 25={password=123456, gender=男, id=25, addr=某地, username=张三三}, 14={password=123456, gender=男, id=14, addr=某地, username=张三三}, 15={password=123456, gender=男, id=15, addr=某地, username=张三三}}
更多
特殊符号
对于特殊符号,可以使用其转义符号,如表
原符号
<
<=
>
>=
&
'
"
转义符号
<
<=
>
>=
&
'
"
示例代码:
1 2 3 <select id ="getEmpListByAssociationStepOne" resultMap ="stepOneEmpList" > select * from emp where id < #{id} </select >
还可以用<![CDATA[ 【被包裹内容】 ]]>
包裹起来,例如:
大于等于:<![CDATA[ >= ]]>
小于等于:<![CDATA[ <= ]]>
模糊条件
分析问题
模糊条件,例如模糊查询,SQL就像这样
1 2 3 SELECT *FROM user WHERE username LIKE '%三%'
那么,来吧!
示例代码:
1 List<User> getUserByLike (@Param("keyWord" ) String keyWord) ;
1 2 3 <select id ="getUserByLike" resultType ="com.kakawanyifan.pojo.User" > select * from user where username like '%#{keyWord}%' </select >
1 System.out.println(userMapper.getUserByLike("四" ));
运行结果:
1 2 3 4 5 6 7 8 9 org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='keyWord', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0). ### The error may exist in com/kakawanyifan/mapper/UserMapper.xml ### The error may involve defaultParameterMap ### The error occurred while setting parameters ### SQL: select * from user where username like '%?%' ### Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='keyWord', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0). 【部分运行结果略】
为什么会报错?
注意看:
1 2 ### SQL: select * from user where username like '%?%' ### Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping
我们写的是'%#{keyWord}%'
,然后我们的#{keyWord}
会被占位符?
代替,这点是没有问题的。
但是#{keyWord}
在一对的单引号中,所以这个成了字符串'%?%'
,所以这个SQL无法被设置参数。
那么,怎么解决?
‘%${keyWord}%’
第一种方法,我们用'%${keyWord}%'
,用$
开头的,这个的作用是字符串拼接。
示例代码:
1 2 3 <select id ="getUserByLike" resultType ="com.kakawanyifan.pojo.User" > select * from user where username like '%${keyWord}%' </select >
1 System.out.println(userMapper.getUserByLike("四" ));
运行结果:
1 [User{id=2, username='李四', password='456', gender='女', addr='天津'}]
但是,这种方法无法避免SQL注入。
关于SQL注入,我们在《10.JDBC》 讨论PreparedStatement
的时候讨论过。
concat(‘%’,#{keyWord},‘%’)
第二种方法,我们利用MySQL的字符串拼接函数concat()
,concat('%',#{keyWord},'%')
。
示例代码:
1 2 3 <select id ="getUserByLike" resultType ="com.kakawanyifan.pojo.User" > select * from user where username like concat('%',#{keyWord},'%') </select >
“%”#{keyWord}“%”
第三种方法,用双引号"%"#{keyWord}"%"
。
示例代码:
1 2 3 <select id ="getUserByLike" resultType ="com.kakawanyifan.pojo.User" > select * from user where username like "%"#{keyWord}"%" </select >
1 System.out.println(userMapper.getUserByLike("四" ));
运行结果:
1 [User{id=2, username='李四', password='456', gender='女', addr='天津'}]
我个人推荐这种方法。
bind标签
第四种方法,利用<bind>
标签。
示例代码:
1 2 3 4 <select id ="getUserByLike" resultType ="com.kakawanyifan.pojo.User" > <bind name ="likeKeyWord" value ="'%' + keyWord + '%'" /> select * from user where username like #{likeKeyWord} </select >
运行结果:
1 [User{id=2, username='李四', password='456', gender='女', addr='天津'}]
IN条件
${}
第一种方法,使用${}
。
如果使用#{}
,则解析后的SQL语句会类似ID in ('1,2,3')
,1,2,3
会看做是一个整体,而正确的应该是ID in (1,2,3)
或ID in ('1','2','3')
。
示例代码:
1 List<User> getUserByIds (@Param("ids" ) String ids) ;
1 2 3 <select id ="getUserByIds" resultType ="com.kakawanyifan.pojo.User" > select * from user where id in (${ids}) </select >
1 System.out.println(userMapper.getUserByIds("1,2,3" ));
运行结果:
1 [User{id=1, username='张三', password='333', gender='男', addr='北京'}, User{id=2, username='李四', password='456', gender='女', addr='天津'}, User{id=3, username='王五', password='789', gender='男', addr='西安'}]
foreach
<foreach>
标签,作用于动态SQL,常用属性有:
collection
:循环的数组或集合
item
:集合或数组中的每一项
separator
:每一项之间的分隔符
open
:开始符
close
:结束符
示例代码:
1 List<User> getUserByIdList (@Param("idList" ) List idList) ;
1 2 3 4 5 6 <select id ="getUserByIdList" resultType ="com.kakawanyifan.pojo.User" > select * from user where id in <foreach collection ="idList" item ="id" separator ="," open ="(" close =")" > #{id} </foreach > </select >
1 2 3 4 5 List<Integer> idList = new ArrayList<>(); idList.add(1 ); idList.add(2 ); idList.add(3 ); System.out.println(userMapper.getUserByIdList(idList));
运行结果:
1 [User{id=1, username='张三', password='333', gender='男', addr='北京'}, User{id=2, username='李四', password='456', gender='女', addr='天津'}, User{id=3, username='王五', password='789', gender='男', addr='西安'}]
获取自增的主键
获取自增的主键,即插入一条数据后,获取该数据的自增主键。
示例代码:
1 int insertUserGenId (@Param("user" ) User user) ;
1 2 3 <insert id ="insertUserGenId" useGeneratedKeys ="true" keyProperty ="id" > insert into user values(null,#{user.username},#{user.password},#{user.gender},#{user.addr}) </insert >
1 2 3 4 5 6 User user = new User(); user.setUsername("张疯" ); user.setPassword("666" ); System.out.println(user); userMapper.insertUserGenId(user); System.out.println(user);
运行结果:
1 2 User{id=0, username='张疯', password='666', gender='null', addr='null'} User{id=28, username='张疯', password='666', gender='null', addr='null'}
我们解释一下映射文件中的两个属性:
useGeneratedKeys
:设置使用自增的主键。
keyProperty
:将获取的自增的主键放在传输的对象的指定属性中。
提一个问题,为什么上述运行的第一次打印,id=0
。
因为基本类型int
的默认值是0
。
题外话,在JDBC中的获取自增主键的方法。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import java.sql.*;public class JdbcDemo { public static void main (String[] args) throws SQLException { String url = "jdbc:mysql://127.0.0.1:3306/test_mybatis" ; String username = "root" ; String password = "MySQL@2022" ; Connection connection = DriverManager.getConnection(url,username,password); Statement statement = connection.createStatement(); statement.execute("insert into user values (null,'姓姓名','密密码','男','地地址')" ,Statement.RETURN_GENERATED_KEYS); ResultSet resultSet = statement.getGeneratedKeys(); if (resultSet.next()){ System.out.println(resultSet.getLong(1 )); } connection.close(); } }
运行结果:
注意statement.execute()
方法,传入了一个参数Statement.RETURN_GENERATED_KEYS
。
自定义映射
自定义字段和属性的映射
对于,字段名和实体类中的属性名不一致:
如果数据库字段名采用_
,Java属性名采取小驼峰,可以利用上文提到的mapUnderscoreToCamelCase
属性配置,此时无需自定义映射。
采取在SQL中给列取别名的方法,以实现resultType
的自动映射,也无需自定义映射。
或者,通过resultMap
自定义映射。
(使用resultMap
的话,即使有部分字段名和属性名一致,也要列出来。也就是全部属性都要列出来。)
假设存在一个新的类NewUser
,依旧是和表user
进行关联。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan.pojo;public class NewUser { private int newId; private String newUsername; private String newPassword; private String newGender; private String newAddr; 【Getter和Setter代码略】 【toString代码略】 }
表user
的结构如下:
1 2 3 4 5 6 7 8 CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar (20 ) DEFAULT NULL , `password` varchar (20 ) DEFAULT NULL , `gender` char (1 ) DEFAULT NULL , `addr` varchar (30 ) DEFAULT NULL , PRIMARY KEY (`id` ) ) ENGINE =InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET =utf8mb4 COLLATE =utf8mb4_0900_ai_ci
示例代码:
1 NewUser getNewUserById (int id) ;
1 2 3 4 5 6 7 8 9 10 <resultMap id ="newUser" type ="com.kakawanyifan.pojo.NewUser" > <id property ="newId" column ="id" > </id > <result property ="newUsername" column ="username" > </result > <result property ="newPassword" column ="password" > </result > <result property ="newGender" column ="gender" > </result > <result property ="newAddr" column ="addr" > </result > </resultMap > <select id ="getNewUserById" resultMap ="newUser" > select id,username,password,gender,addr from user where id = #{id} </select >
1 System.out.println(newUserMapper.getNewUserById(1 ));
运行结果:
1 NewUser{newId=1, newUsername='张三', newPassword='333', newGender='男', newAddr='北京'}
解释说明:
<resultMap>
标签,有两个属性:
id
:自定义映射的唯一标识,不能重复
type
:要映射的实体类的类型,Java中的名字。
<resultMap>
标签,有两个子标签:
<id>
标签,<resultMap>
的子标签,用于设置主键的映射关系。
<result>
标签,<resultMap>
的子标签,用于设置普通字段的映射关系(主键也可以用<result>
标签)。
<id>
标签和<result>
标签,都有两个属性:
property
:设射关系中实体类中的属性名,Java中的名字。
column
:映射关系中表中的字段名,数据库中的名字。
多对一映射处理
假设现在存在两张表,emp
员工表和dept
部门表,其表结构如下:
1 2 3 4 5 6 7 create table emp( id int auto_increment comment '主键' primary key , name varchar (32 ) null comment '员工姓名' , dept_id int null comment '部门主键' );
1 2 3 4 5 6 create table dept( id int auto_increment comment '主键' primary key , name varchar (32 ) null comment '部门名称' );
多个员工会隶属于一个部门,这就是多对一。
现在查询员工信息以及员工所对应的部门信息。
级联方式
建立Pojo类,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan.pojo;public class Emp { private Integer id; private String name; private Integer deptId; private Dept dept; 【Getter和Setter代码略】 【toString代码略】 }
1 2 3 4 5 6 7 8 9 10 11 package com.kakawanyifan.pojo;public class Dept { private Integer id; private String name; 【Getter和Setter代码略】 【toString代码略】 }
定义MyBatis相关的接口、映射,示例代码:
1 Emp getEmpInfo (@Param("id" ) Integer id) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <resultMap id ="empInfo" type ="com.kakawanyifan.pojo.Emp" > <id property ="id" column ="e_id" > </id > <result property ="name" column ="e_name" > </result > <result property ="deptId" column ="d_id" > </result > <result property ="dept.id" column ="d_id" > </result > <result property ="dept.name" column ="d_name" > </result > </resultMap > <select id ="getEmpInfo" resultMap ="empInfo" > SELECT e.id as e_id, e.name as e_name, e.dept_id as e_dept_id, d.id as d_id, d.name as d_name from emp e left join dept d on d.id = e.dept_id where e.id = #{id} </select >
1 System.out.println(resultMapMapper.getEmpInfo(1 ));
运行结果:
1 Emp{id=1, name='张三', deptId=1, dept=Dept{id=1, name='部门一'}}
association
除了上述方法,还可以利用<association>
标签,<resultMap>
的子标签,用于处理多对一的映射关系。
在这里,我们会用到的属性有:
property
:需要处理多对的映射关系的属性名
javaType
:该属性的类型,MyBatis通常可以推断类型,但建议指定。
示例代码:
1 Emp getEmpInfoByAssociation (@Param("id" ) Integer id) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <resultMap id ="empInfoAssociation" type ="com.kakawanyifan.pojo.Emp" > <id property ="id" column ="e_id" > </id > <result property ="name" column ="e_name" > </result > <result property ="deptId" column ="d_id" > </result > <association property ="dept" javaType ="com.kakawanyifan.pojo.Dept" > <result property ="id" column ="d_id" > </result > <result property ="name" column ="d_name" > </result > </association > </resultMap > <select id ="getEmpInfoByAssociation" resultMap ="empInfoAssociation" > SELECT e.id as e_id, e.name as e_name, e.dept_id as e_dept_id, d.id as d_id, d.name as d_name from emp e left join dept d on d.id = e.dept_id where e.id = #{id} </select >
1 System.out.println(resultMapMapper.getEmpInfoByAssociation(1 ));
运行结果:
1 Emp{id=1, name='张三', deptId=1, dept=Dept{id=1, name='部门一'}}
分步查询
分步查询,同样依赖于<association>
标签,这里会用到的属性,除了property
,还有:
select
:分步查询中下一步的SQL的唯一标识,可以是namespace.SQLId
或Mapper接口的全限定名.方法名
。
column
:分步查询的条件
示例代码:
1 2 3 Emp getEmpByAssociationStepOne (@Param("id" ) Integer id) ;Dept getDeptByAssociationStepTwo (@Param("id" ) Integer id) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <resultMap id ="stepOneEmp" type ="com.kakawanyifan.pojo.Emp" > <id property ="id" column ="id" > </id > <result property ="name" column ="name" > </result > <result property ="deptId" column ="dept_id" > </result > <association property ="dept" select ="com.kakawanyifan.mapper.ResultMapMapper.getDeptByAssociationStepTwo" column ="dept_id" > </association > </resultMap > <select id ="getEmpByAssociationStepOne" resultMap ="stepOneEmp" > select * from emp where id = #{id} </select > <resultMap id ="stepTwoDept" type ="com.kakawanyifan.pojo.Dept" > <id property ="id" column ="id" > </id > <result property ="name" column ="name" > </result > </resultMap > <select id ="getDeptByAssociationStepTwo" resultMap ="stepTwoDept" > select * from dept where id = #{id} </select >
1 System.out.println(resultMapMapper.getEmpByAssociationStepOne(1 ));
运行结果:
1 2 3 4 5 6 7 DEBUG 06-13 19:22:43,424 ==> Preparing: select * from emp where id = ? (BaseJdbcLogger.java:137) DEBUG 06-13 19:22:43,448 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137) DEBUG 06-13 19:22:43,468 ====> Preparing: select * from dept where id = ? (BaseJdbcLogger.java:137) DEBUG 06-13 19:22:43,468 ====> Parameters: 11(Integer) (BaseJdbcLogger.java:137) DEBUG 06-13 19:22:43,469 <==== Total: 1 (BaseJdbcLogger.java:137) DEBUG 06-13 19:22:43,471 <== Total: 1 (BaseJdbcLogger.java:137) Emp{id=1, name='张三', deptId=11, dept=Dept{id=11, name='部门一'}}
如运行结果所示,依次执行了两个SQL:
select * from emp where id = ?
select * from dept where id = ?
即,进行了分步查询。特别的,如果我们第一步的查询是一个范围,则第二步的查询可能会被执行多次。
示例代码:
1 List<Emp> getEmpListByAssociationStepOne (@Param("id" ) Integer id) ;
1 2 3 4 5 6 7 8 9 10 11 <resultMap id ="stepOneEmpList" type ="com.kakawanyifan.pojo.Emp" > <id property ="id" column ="id" > </id > <result property ="name" column ="name" > </result > <result property ="deptId" column ="dept_id" > </result > <association property ="dept" select ="com.kakawanyifan.mapper.ResultMapMapper.getDeptByAssociationStepTwo" column ="dept_id" > </association > </resultMap > <select id ="getEmpListByAssociationStepOne" resultMap ="stepOneEmpList" > select * from emp where id < #{id} </select >
示例代码:
1 System.out.println(resultMapMapper.getEmpListByAssociationStepOne(10 ));
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 DEBUG 06-13 19:31:25,038 ==> Preparing: select * from emp where id < ? (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,070 ==> Parameters: 10(Integer) (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,105 ====> Preparing: select * from dept where id = ? (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,106 ====> Parameters: 11(Integer) (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,108 <==== Total: 1 (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,111 ====> Preparing: select * from dept where id = ? (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,112 ====> Parameters: 1(Integer) (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,113 <==== Total: 0 (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,114 ====> Preparing: select * from dept where id = ? (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,115 ====> Parameters: 2(Integer) (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,116 <==== Total: 1 (BaseJdbcLogger.java:137) DEBUG 06-13 19:31:25,117 <== Total: 6 (BaseJdbcLogger.java:137) [Emp{id=1, name='张三', deptId=11, dept=Dept{id=11, name='部门一'}}, Emp{id=2, name='李四', deptId=1, dept=null}, Emp{id=3, name='王五', deptId=1, dept=null}, Emp{id=4, name='Tom', deptId=2, dept=Dept{id=2, name='部门二'}}, Emp{id=5, name='Jerry', deptId=2, dept=Dept{id=2, name='部门二'}}, Emp{id=6, name='Lucy', deptId=2, dept=Dept{id=2, name='部门二'}}]
但好像有问题?按照一般的理解,select * from dept where id = ? Parameters: 1(Integer)
,应该需要被执行两次;select * from dept where id = ? Parameters: 2(Integer)
,应该需要被执行三次。
我们看到,都只执行了一次。
这就是MyBatis的缓存,我们在下文会讨论。
多个参数的Associate
如果是多个条件的Associate,可以用形如这种方式,传递多个参数:
1 column="{empId=id,deptId=dept_id}"
假设,存在一张表salary
,表结构如下:
1 2 3 4 5 6 7 create table salary( id int auto_increment primary key , money decimal null , emp_id int null , dept_id int null );
我们需要通过emp_id
和dept_id
两个字段进行关联查询,示例代码:
1 2 3 4 5 6 7 SELECT e.id as e_id, e.name as e_name, e.dept_id as e_dept_id, s.money as s_money from emp e left join salary s on e.id = s.emp_id and e.dept_id = s.dept_id where e.id = 1
新建实体类,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan.pojo;public class Emp { private Integer id; private String name; private Integer deptId; private Salary salary; 【Getter和Setter代码略】 【toString代码略】 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.kakawanyifan.pojo;import java.math.BigDecimal;public class Salary { Integer id; BigDecimal money; Integer empId; Integer deptId; 【Getter和Setter代码略】 【toString代码略】 }
定义MyBatis相关的接口、映射,示例代码:
1 2 3 Emp getEmpSalary (@Param("id" ) Integer id) ;Salary getSalary (@Param("empId" ) Integer empId,@Param ("deptId" ) Integer deptId) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <resultMap id ="empSalary" type ="com.kakawanyifan.pojo.Emp" > <id property ="id" column ="id" > </id > <result property ="name" column ="name" > </result > <result property ="deptId" column ="dept_id" > </result > <association property ="salary" select ="com.kakawanyifan.mapper.ResultMapMapper.getSalary" column ="{empId=id,deptId=dept_id}" > </association > </resultMap > <select id ="getEmpSalary" resultMap ="empSalary" > select * from emp where id = #{id} </select > <select id ="getSalary" resultType ="com.kakawanyifan.pojo.Salary" > select * from salary where emp_id = #{empId} and dept_id = #{deptId} </select >
1 System.out.println(resultMapMapper.getEmpSalary(1 ));
运行结果:
1 2 3 4 5 6 7 DEBUG 06-14 07:27:15,105 ==> Preparing: select * from emp where id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 07:27:15,125 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 07:27:15,146 ====> Preparing: select * from salary where emp_id = ? and dept_id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 07:27:15,146 ====> Parameters: 1(Integer), 11(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 07:27:15,148 <==== Total: 1 (BaseJdbcLogger.java:137) DEBUG 06-14 07:27:15,150 <== Total: 1 (BaseJdbcLogger.java:137) Emp{id=1, name='张三', deptId=11, salary=Salary{id=1, money=100, empId=null, deptId=null}}
一对多映射处理
一对多,例如一个部门有多位员工。
建实体类,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan.pojo;import java.util.List;public class Dept { private Integer id; private String name; private List<Emp> empList; 【Getter和Setter代码略】 【toString代码略】 }
1 2 3 4 5 6 7 8 9 10 11 package com.kakawanyifan.pojo;public class Emp { private Integer id; private String name; 【Getter和Setter代码略】 【toString代码略】 }
定义MyBatis相关的接口、映射,示例代码:
1 2 3 Dept getDeptEmp (@Param("id" ) Integer id) ;Emp getEmpByDeptId (@Param("deptId" ) Integer deptId) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <resultMap id ="deptEmpMap" type ="com.kakawanyifan.pojo.Dept" > <id property ="id" column ="id" > </id > <result property ="name" column ="name" > </result > <association property ="empList" select ="com.kakawanyifan.mapper.ResultMapMapper.getEmpByDeptId" column ="id" > </association > </resultMap > <select id ="getDeptEmp" resultMap ="deptEmpMap" > select * from dept where id = #{id} </select > <select id ="getEmpByDeptId" resultType ="com.kakawanyifan.pojo.Emp" > select * from emp where dept_id = #{deptId} </select >
1 System.out.println(resultMapMapper.getDeptEmp(2 ));
运行结果:
1 Dept{id=2, name='部门二', empList=[Emp{id=4, name='Tom'}, Emp{id=5, name='Jerry'}, Emp{id=6, name='Lucy'}]}
collection
除了上述的<association>
标签,还有一个标签<collection>
,专门用来处理一对多的映射关系。
该标签的常用属性是ofType
,表示集合中存储的数据的类型。
示例代码:
1 Dept getDeptEmpList (@Param("id" ) Integer id) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <resultMap id ="deptEmpListMapper" type ="com.kakawanyifan.pojo.Dept" > <id property ="id" column ="d_id" > </id > <result property ="name" column ="d_name" > </result > <collection property ="empList" ofType ="com.kakawanyifan.pojo.Emp" > <id property ="id" column ="e_id" > </id > <result property ="name" column ="e_name" > </result > </collection > </resultMap > <select id ="getDeptEmpList" resultMap ="deptEmpListMapper" > select d.id as d_id, d.name as d_name, e.id as e_id, e.name as e_name from dept d left join emp e on d.id = e.dept_id where d.id = #{id} </select >
1 System.out.println(resultMapMapper.getDeptEmpList(2 ));
运行结果:
1 2 3 4 DEBUG 06-14 21:15:27,868 ==> Preparing: select d.id as d_id, d.name as d_name, e.id as e_id, e.name as e_name from dept d left join emp e on d.id = e.dept_id where d.id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 21:15:27,895 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 21:15:27,915 <== Total: 3 (BaseJdbcLogger.java:137) Dept{id=2, name='部门二', empList=[Emp{id=4, name='Tom'}, Emp{id=5, name='Jerry'}, Emp{id=6, name='Lucy'}]}
延迟加载
现象和设置方法
上文的<association>
标签的分步查询,还有一个优点:实现延迟加载。
延迟加载,默认是关闭的,需要通过MyBatis的配置文件的<setting>
标签,设置lazyLoadingEnabled
为true
以开启延迟加载。
例如,如果我们只获取部门的名称信息,这个是不需要进行第二步操作的。
为开启延迟加载
示例代码:
1 Dept getDeptEmp (@Param("id" ) Integer id) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <resultMap id ="deptEmpMap" type ="com.kakawanyifan.pojo.Dept" > <id property ="id" column ="id" > </id > <result property ="name" column ="name" > </result > <association property ="empList" select ="com.kakawanyifan.mapper.ResultMapMapper.getEmpByDeptId" column ="id" > </association > </resultMap > <select id ="getDeptEmp" resultMap ="deptEmpMap" > select * from dept where id = #{id} </select > <select id ="getEmpByDeptId" resultType ="com.kakawanyifan.pojo.Emp" > select * from emp where dept_id = #{deptId} </select >
1 System.out.println(resultMapMapper.getDeptEmp(2 ).getName());
运行结果:
1 2 3 4 5 6 7 DEBUG 06-14 21:32:50,954 ==> Preparing: select * from dept where id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 21:32:50,978 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 21:32:50,994 ====> Preparing: select * from emp where dept_id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 21:32:50,994 ====> Parameters: 2(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 21:32:50,996 <==== Total: 3 (BaseJdbcLogger.java:137) DEBUG 06-14 21:32:50,998 <== Total: 1 (BaseJdbcLogger.java:137) 部门二
第二步的SQL,还是查了。
开启延迟加载
设置lazyLoadingEnabled
为true
,开启延迟加载。
1 2 3 4 <settings > <setting name ="lazyLoadingEnabled" value ="true" /> </settings >
示例代码:
1 System.out.println(resultMapMapper.getDeptEmp(2 ).getName());
运行结果:
1 2 3 4 DEBUG 06-14 21:37:24,405 ==> Preparing: select * from dept where id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 21:37:24,430 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 21:37:24,517 <== Total: 1 (BaseJdbcLogger.java:137) 部门二
此时,没有查第二步的SQL。
fetchType
fetchType
:当开启了全局的延迟加载之后,可以通过该属性进行细粒度的控制,有两个取值:
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <resultMap id ="deptEmpMap" type ="com.kakawanyifan.pojo.Dept" > <id property ="id" column ="id" > </id > <result property ="name" column ="name" > </result > <association property ="empList" select ="com.kakawanyifan.mapper.ResultMapMapper.getEmpByDeptId" column ="id" fetchType ="eager" > </association > </resultMap > <select id ="getDeptEmp" resultMap ="deptEmpMap" > select * from dept where id = #{id} </select > <select id ="getEmpByDeptId" resultType ="com.kakawanyifan.pojo.Emp" > select * from emp where dept_id = #{deptId} </select >
1 System.out.println(resultMapMapper.getDeptEmp(2 ).getName());
运行结果:
1 2 3 4 5 6 7 DEBUG 06-14 21:39:25,029 ==> Preparing: select * from dept where id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 21:39:25,055 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 21:39:25,078 ====> Preparing: select * from emp where dept_id = ? (BaseJdbcLogger.java:137) DEBUG 06-14 21:39:25,078 ====> Parameters: 2(Integer) (BaseJdbcLogger.java:137) DEBUG 06-14 21:39:25,080 <==== Total: 3 (BaseJdbcLogger.java:137) DEBUG 06-14 21:39:25,082 <== Total: 1 (BaseJdbcLogger.java:137) 部门二
动态SQL
动态SQL,可以通过标签解决拼接SQL语句时的痛点。
if
<if>
标签,通过test属性的表达式进行判断,若表达式的结果为true,则标签中的内容会被拼接;反之标签中的内容不会被拼接。
示例代码:
1 2 3 User getUserWithIf (@Param("userName" ) String userName, @Param ("passWord" ) String passWord, @Param ("gender" ) String gender) ;
1 2 3 4 5 6 7 8 9 10 11 12 <select id ="getUserWithIf" resultType ="com.kakawanyifan.pojo.User" > select * from user where 1=1 <if test ="userName != null and userName !=''" > and username = #{userName} </if > <if test ="passWord != null and passWord !=''" > and password = #{passWord} </if > <if test ="gender != null and gender !=''" > and gender = #{gender} </if > </select >
1 System.out.println(labelMapper.getUserWithIf("" ,"333" ,"" ));
运行结果:
1 2 3 4 DEBUG 06-15 07:57:30,255 ==> Preparing: select * from user where 1=1 and password = ? (BaseJdbcLogger.java:137) DEBUG 06-15 07:57:30,282 ==> Parameters: 333(String) (BaseJdbcLogger.java:137) DEBUG 06-15 07:57:30,336 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
在上述例子中,我们在where
后面添加了一个恒成立条件1=1
,如果不添加,会导致where
和and
连用,语法错误。
where
如果<where>
标签中没有内容,不会在SQL中添加where
关键字;
如果<where>
标签中有内容,则添加关键字,并自动去除前缀多余的and
/or
。
示例代码:
1 2 3 User getUserWithWhere (@Param("userName" ) String userName, @Param ("passWord" ) String passWord, @Param ("gender" ) String gender) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="getUserWithWhere" resultType ="com.kakawanyifan.pojo.User" > select * from user <where > <if test ="userName != null and userName !=''" > and username = #{userName} </if > <if test ="passWord != null and passWord !=''" > and password = #{passWord} </if > <if test ="gender != null and gender !=''" > and gender = #{gender} </if > </where > </select >
1 System.out.println(labelMapper.getUserWithWhere("" ,"333" ,"" ));
运行结果:
1 2 3 4 DEBUG 06-15 08:09:33,406 ==> Preparing: select * from user WHERE password = ? (BaseJdbcLogger.java:137) DEBUG 06-15 08:09:33,430 ==> Parameters: 333(String) (BaseJdbcLogger.java:137) DEBUG 06-15 08:09:33,482 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
注意 :<where>
标签不能去掉后缀多余的and
/or
。
错误示范:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="getUserWithWhere" resultType ="com.kakawanyifan.pojo.User" > select * from user <where > <if test ="userName != null and userName !=''" > username = #{userName} and </if > <if test ="passWord != null and passWord !=''" > password = #{passWord} and </if > <if test ="gender != null and gender !=''" > gender = #{gender} and </if > </where > </select >
有些资料说<where>
标签是否生效,取决于<where>
标签中的<if>
条件是否"都不满足"或"有满足"的。
这个不够准确,应该是取决于<where>
标签中是否有内容,只是,是否有内容,经常由<if>
标签控制。
例如,在<where>
标签中没有<if>
标签,示例代码:
1 2 3 User getUserWithWhere (@Param("userName" ) String userName, @Param ("passWord" ) String passWord, @Param ("gender" ) String gender) ;
1 2 3 4 5 6 <select id ="getUserWithWhere" resultType ="com.kakawanyifan.pojo.User" > select * from user <where > and password = #{passWord} </where > </select >
1 System.out.println(labelMapper.getUserWithWhere("" ,"333" ,"" ));
运行结果:
1 2 3 4 DEBUG 06-15 08:16:20,131 ==> Preparing: select * from user WHERE password = ? (BaseJdbcLogger.java:137) DEBUG 06-15 08:16:20,155 ==> Parameters: 333(String) (BaseJdbcLogger.java:137) DEBUG 06-15 08:16:20,175 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
trim
<trim>
标签用于去掉或添加内容,常用属性:
prefix
:前缀,添加
suffix
:后缀,添加
prefixOverrides
:前缀,去除
suffixOverrides
:后缀,去除
和<where>
标签一样,<trim>
执行与否,取决于是否有内容。
示例代码:
1 2 3 User getUserWithTrim (@Param("userName" ) String userName, @Param ("passWord" ) String passWord, @Param ("gender" ) String gender) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="getUserWithTrim" resultType ="com.kakawanyifan.pojo.User" > select * from user <trim prefix ="where" suffixOverrides ="and|or" > <if test ="userName != null and userName !=''" > username = #{userName} and </if > <if test ="passWord != null and passWord !=''" > password = #{passWord} and </if > <if test ="gender != null and gender !=''" > gender = #{gender} and </if > </trim > </select >
1 System.out.println(labelMapper.getUserWithTrim("" ,"333" ,"" ));
运行结果:
1 2 3 4 DEBUG 06-15 08:21:09,014 ==> Preparing: select * from user where password = ? (BaseJdbcLogger.java:137) DEBUG 06-15 08:21:09,036 ==> Parameters: 333(String) (BaseJdbcLogger.java:137) DEBUG 06-15 08:21:09,080 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
choose-when-otherwise
<choose> ... <when> ... <otherwise>
相当于if ... else if ... else ...
示例代码:
1 2 3 User getUserWithChoose (@Param("userName" ) String userName, @Param ("passWord" ) String passWord, @Param ("gender" ) String gender) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <select id ="getUserWithChoose" resultType ="com.kakawanyifan.pojo.User" > select * from user <where > <choose > <when test ="userName != null and userName != ''" > username = #{userName} </when > <when test ="passWord != null and passWord != ''" > password = #{passWord} </when > <when test ="gender != null and gender != ''" > gender = #{gender} </when > <otherwise > id = 1 </otherwise > </choose > </where > </select >
示例代码:
1 2 3 System.out.println(labelMapper.getUserWithChoose("" ,"333" ,"" )); System.out.println(labelMapper.getUserWithChoose("" ,"" ,"" ));
运行结果:
1 2 3 4 5 6 7 8 DEBUG 06-15 08:28:04,293 ==> Preparing: select * from user WHERE password = ? (BaseJdbcLogger.java:137) DEBUG 06-15 08:28:04,317 ==> Parameters: 333(String) (BaseJdbcLogger.java:137) DEBUG 06-15 08:28:04,368 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} DEBUG 06-15 08:28:04,370 ==> Preparing: select * from user WHERE id = 1 (BaseJdbcLogger.java:137) DEBUG 06-15 08:28:04,370 ==> Parameters: (BaseJdbcLogger.java:137) DEBUG 06-15 08:28:04,372 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
如日志打印的SQL所示,相当于if ... else if ... else ...
,只会执行其中一个。
foreach
<foreach>
标签,这个我们在上文已经用过了,当时用于解决"IN条件"的查询,标签的属性有:
collection
:循环的数组或集合
item
:集合或数组中的每一项
separator
:每一项之间的分隔符
open
:开始符
close
:结束符
这里我们举一个批量添加的例子。
示例代码:
1 int insertUserBatch (@Param("userList" ) List<User> userList) ;
1 2 3 4 5 6 <insert id ="insertUserBatch" > insert into user values <foreach collection ="userList" item ="user" separator ="," > (null,#{user.username},#{user.password},#{user.gender},#{user.addr}) </foreach > </insert >
1 2 3 4 5 6 List<User> userList = new ArrayList<>(); User u1 = new User(0 ,"一号" ,"11" ,"男" ,"住在这" ); User u2 = new User(0 ,"二号" ,"22" ,"女" ,"住在那" ); userList.add(u1); userList.add(u2); System.out.println(labelMapper.insertUserBatch(userList));
运行结果:
1 2 3 DEBUG 06-15 13:15:19,018 ==> Preparing: insert into user values (null,?,?,?,?) , (null,?,?,?,?) (BaseJdbcLogger.java:137) DEBUG 06-15 13:15:19,039 ==> Parameters: 一号(String), 11(String), 男(String), 住在这(String), 二号(String), 22(String), 女(String), 住在那(String) (BaseJdbcLogger.java:137) DEBUG 06-15 13:15:19,043 <== Updates: 2 (BaseJdbcLogger.java:137)
注意!如果被循环的内容是空或者元素个数为0,会报错。 解决办法是,应该将判断集合是否为空的代码尽量放在程序里,如果非要让Mybatis来处理的话,可以参考下面的方式:
1 2 3 4 5 6 <if test ="list != null and list.length>0" > do something </if > <if test ="list == null or list.length==0" > and 1=0 </if >
如果参数为数组(object[]
),采取.length
这种方式。 如果参数为列表,采取.size()
这种方式。 and 1=0
,是为了不产生逻辑错误。
include-sql
通过<sql>
标签声明一段SQL片段,在使用的地方通过<include>
标签进行引入。
示例代码:
1 User getUserById (@Param("id" ) String id) ;
1 2 3 4 <sql id ="userCols" > id,username,password,gender,addr</sql > <select id ="getUserById" resultType ="com.kakawanyifan.pojo.User" > select <include refid ="userCols" > </include > from user where id = #{id} </select >
1 System.out.println(labelMapper.getUserById("1" ));
运行结果:
1 2 3 4 DEBUG 06-15 13:29:35,682 ==> Preparing: select id,username,password,gender,addr from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 13:29:35,706 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 13:29:35,725 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
缓存
一级缓存
概述
一级缓存,默认开启。
一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问。
同一个SqlSession,执行两个相同的查询,示例代码:
1 2 3 4 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); System.out.println(userMapper.getUserById("1" ));
运行结果:
1 2 3 4 5 DEBUG 06-15 20:55:24,535 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 20:55:24,568 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 20:55:24,591 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} User{id=1, username='张三', password='333', gender='男', addr='北京'}
解释说明:第二次的查询,没有执行SQL。
不同的SqlSession,执行相同的查询,示例代码:
1 2 3 4 5 6 7 8 9 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); SqlSession sqlSession2 = sqlSessionFactory.openSession(true ); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class ) ; System.out.println(userMapper2.getUserById("1" ));
运行结果:
1 2 3 4 5 6 7 8 DEBUG 06-15 20:58:22,320 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 20:58:22,345 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 20:58:22,364 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} DEBUG 06-15 20:58:22,383 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 20:58:22,384 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 20:58:22,385 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
失效情况
使一级缓存失效的四种情况:
不同的SqlSession对应不同的一级缓存
同一个SqlSession但是查询条件不同
同一个SqlSession两次查询期间执行了任何一次增删改操作
同一个SqlSession两次查询期间手动清空了缓存 sqlSession.clearCache();
。
示例代码:
1 2 3 4 5 6 7 8 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); LabelMapper labelMapper = sqlSession.getMapper(LabelMapper.class ) ; List<User> list = new ArrayList<>(); list.add(new User(0 ,"1" ,"2" ,"3" ,"4" )); labelMapper.insertUserBatch(list); System.out.println(userMapper.getUserById("1" ));
运行结果:
1 2 3 4 5 6 7 8 9 10 11 DEBUG 06-15 21:01:40,498 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:01:40,521 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:01:40,540 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} DEBUG 06-15 21:01:40,557 ==> Preparing: insert into user values (null,?,?,?,?) (BaseJdbcLogger.java:137) DEBUG 06-15 21:01:40,557 ==> Parameters: 1(String), 2(String), 3(String), 4(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:01:40,561 <== Updates: 1 (BaseJdbcLogger.java:137) DEBUG 06-15 21:01:40,562 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:01:40,562 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:01:40,563 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
解释说明:任意一次!即使我们分析认为不会影响缓存,缓存也会被情理。
二级缓存
二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取。
二级缓存是需要手动开启的。
二级缓存生效的条件:
在核心配置文件中,设置全局配置属性cacheEnabled="true"
,默认为true
,不需要设置
在映射文件中设置标签<cache />
二级缓存必须在SqlSession关闭或提交之后有效
查询的数据所转换的实体类类型必须实现序列化的接口
二级缓存失效的情况:
两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效
示例代码:
1 User getUserById (String id) ;
1 2 3 4 5 6 7 8 <?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.kakawanyifan.mapper.UserMapper" > <cache > </cache > <select id ="getUserById" resultType ="com.kakawanyifan.pojo.User" > select * from user where id = #{id} </select > </mapper >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan.pojo;import java.io.Serializable;public class User implements Serializable { private int id; private String username; private String password; private String gender; private String addr; 【Getter和Setter代码略】 【toString代码略】 }
1 2 3 4 5 6 7 8 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); sqlSession.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(true ); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class ) ; System.out.println(userMapper2.getUserById("1" ));
运行结果:
1 2 3 4 5 6 7 8 DEBUG 06-15 21:11:34,311 Cache Hit Ratio [com.kakawanyifan.mapper.UserMapper]: 0.0 (LoggingCache.java:60) DEBUG 06-15 21:11:34,647 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:11:34,666 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:11:34,683 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} WARN 06-15 21:11:34,688 As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66 (SerialFilterChecker.java:45) DEBUG 06-15 21:11:34,690 Cache Hit Ratio [com.kakawanyifan.mapper.UserMapper]: 0.5 (LoggingCache.java:60) User{id=1, username='张三', password='333', gender='男', addr='北京'}
缓存查询的顺序
先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。
如果二级缓存没有命中,再查询一级缓存
如果一级缓存也没有命中,则查询数据库
使用缓存的问题
来看一个视频。
解释说明:
第一次执行,只打印了一个SQL,这个是没问题的,MyBatis的缓存。
第二次执行,在执行第一个SQL之后,期间我去修改了表里的数据,然后再执行,结果这次利用了缓存,第二次查询的结果实际上就错了。
解决方法有:
关闭全局缓存
指定某一个SQL不用缓存
关闭全局缓存
1 2 3 <settings > <setting name ="cacheEnabled" value ="false" /> </settings >
1 2 3 4 5 6 7 8 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); sqlSession.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(true ); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class ) ; System.out.println(userMapper2.getUserById("1" ));
运行结果:
1 2 3 4 5 6 7 8 DEBUG 06-15 21:28:12,155 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:28:12,182 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:28:12,202 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} DEBUG 06-15 21:28:12,204 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:28:12,204 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:28:12,206 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
示例代码:
1 2 3 4 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); System.out.println(userMapper.getUserById("1" ));
运行结果:
1 2 3 4 5 6 7 8 DEBUG 06-15 21:29:53,043 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:29:53,069 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:29:53,090 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} DEBUG 06-15 21:29:53,092 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:29:53,092 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:29:53,094 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
指定某一个SQL不用缓存
注意!useCache="false"
。
1 2 3 <select id ="getUserById" resultType ="com.kakawanyifan.pojo.User" useCache ="false" > select * from user where id = #{id} </select >
有些资料会说,设置缓存作用范围为STATEMENT
级别,示例代码:
1 2 3 <settings > <setting name ="localCacheScope" value ="STATEMENT" /> </settings >
1 2 3 4 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); System.out.println(userMapper.getUserById("1" ));
运行结果:
1 2 3 4 5 6 7 8 9 10 DEBUG 06-15 21:23:29,769 Cache Hit Ratio [com.kakawanyifan.mapper.UserMapper]: 0.0 (LoggingCache.java:60) DEBUG 06-15 21:23:30,167 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:23:30,186 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:23:30,204 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} DEBUG 06-15 21:23:30,206 Cache Hit Ratio [com.kakawanyifan.mapper.UserMapper]: 0.0 (LoggingCache.java:60) DEBUG 06-15 21:23:30,206 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:23:30,206 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:23:30,207 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'}
但此时,二级缓存依然有效,示例代码:
1 2 3 4 5 6 7 8 SqlSession sqlSession = sqlSessionFactory.openSession(true ); UserMapper userMapper = sqlSession.getMapper(UserMapper.class ) ; System.out.println(userMapper.getUserById("1" )); sqlSession.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(true ); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class ) ; System.out.println(userMapper2.getUserById("1" ));
运行结果:
1 2 3 4 5 6 7 8 DEBUG 06-15 21:25:25,596 Cache Hit Ratio [com.kakawanyifan.mapper.UserMapper]: 0.0 (LoggingCache.java:60) DEBUG 06-15 21:25:25,936 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:137) DEBUG 06-15 21:25:25,959 ==> Parameters: 1(String) (BaseJdbcLogger.java:137) DEBUG 06-15 21:25:25,978 <== Total: 1 (BaseJdbcLogger.java:137) User{id=1, username='张三', password='333', gender='男', addr='北京'} WARN 06-15 21:25:25,982 As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66 (SerialFilterChecker.java:45) DEBUG 06-15 21:25:25,985 Cache Hit Ratio [com.kakawanyifan.mapper.UserMapper]: 0.5 (LoggingCache.java:60) User{id=1, username='张三', password='333', gender='男', addr='北京'}
MyBatis Generator
概述
MyBatis Generator,代码生成器,用于自动生成Java实体类
、Mapper接口
、Mapper映射文件
等。
在有些资料中的"逆向工程",指的就是这个。
正向工程,先创建Java实体类,由框架负责根据实体类生成数据库表,例如Hibernate
逆向工程,先创建数据库表,由框架负责根据数据库表,反向生成Java实体类
、Mapper接口
、Mapper映射文件
。
插件及其属性配置
在Maven的POM文件的根节点,添加mybatis-generator-maven-plugin
,并对该插件的属性进行配置。
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 <build > <plugins > <plugin > <groupId > org.mybatis.generator</groupId > <artifactId > mybatis-generator-maven-plugin</artifactId > <version > 1.4.1</version > <configuration > <overwrite > true</overwrite > <includeCompileDependencies > true</includeCompileDependencies > </configuration > </plugin > </plugins > </build >
属性配置:
<configurationFile>
,生成内容的配置文件地址。
默认src/main/resources/generatorConfig.xml
。
<overwrite>
,是否覆盖上次生成的内容。
默认不覆盖。
如果设置为覆盖后,只会覆盖Java实体类
和Mapper接口
、对于Mapper映射文件
不会覆盖,而是追加,这样做的目的是防止用户自己写的SQL被MyBatis-Generator覆盖。
<includeCompileDependencies>
,是否利用当前POM的文件中的依赖
默认不利用
配置这个之后,我们不需要在<dependencies>
标签中重复配置依赖。
有部分资料说还需要一个依赖mybatis-generator-core
,如下:
1 2 3 4 5 <dependency > <groupId > org.mybatis.generator</groupId > <artifactId > mybatis-generator-core</artifactId > <version > 1.4.1</version > </dependency >
实际上,MyBatis-Generator没有提到说要配置这个依赖;并且不配置,也可以正常使用。
生成内容的配置
在生成器属性的配置中,有一个标签<configurationFile>
,生成内容的配置文件地址。
现在,我们讨论一下生成内容的配置。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" > <generatorConfiguration > <properties resource ="application-dev.properties" /> <context id ="WHATEVER" targetRuntime ="MyBatis3Simple" > <jdbcConnection driverClass ="com.mysql.cj.jdbc.Driver" connectionURL ="jdbc:mysql://127.0.0.1:3306/test_mybatis" userId ="root" password ="MySQL@2022" > </jdbcConnection > <javaModelGenerator targetPackage ="com.kakawanyifan.pojo" targetProject ="./src/main/java" > <property name ="enableSubPackages" value ="true" /> <property name ="trimStrings" value ="true" /> </javaModelGenerator > <sqlMapGenerator targetPackage ="com.kakawanyifan.mapper" targetProject ="./src/main/resources" > <property name ="enableSubPackages" value ="true" /> </sqlMapGenerator > <javaClientGenerator type ="XMLMAPPER" targetPackage ="com.kakawanyifan.mapper" targetProject ="./src/main/java" > <property name ="enableSubPackages" value ="true" /> </javaClientGenerator > <table tableName ="dept" domainObjectName ="Dept" /> <table tableName ="emp" domainObjectName ="Emp" /> <table tableName ="salary" domainObjectName ="Salary" /> </context > </generatorConfiguration >
properties
<properties>
,引入外部文件。
1 2 <properties resource ="jdbc.properties" />
与之不同的是,在MyBatis的配置文件中的<properties>
,我们不但可以通过resource
属性引入外部文件,还可以配置子标签<property>
。
context
<context>
标签的常见属性有:
id
: 无要求,保证多个context
的id
不重复就行。
targetRuntime
:常见取值有两个。
MyBatis3Simple
:生成基本的CRUD
MyBatis3
:生成带条件的CRUD
<context>
标签对于其内部的子标签的顺序也有要求:
property
plugin
commentGenerator
jdbcConnection
javaTypeResolver
javaModelGenerator
sqlMapGenerator
javaClientGenerator
table
plugin
<plugin>
,配置插件,例如
1 <plugin type ="org.mybatis.generator.plugins.EqualsHashCodePlugin" />
该插件给生成的Java模型对象增加了equals和hashCode方法
<commentGenerator>
,用来配置生成的注释,默认是生成注释和时间戳。
如图:
如果想保留注释和时间戳: 可以不配置<commentGenerator>
。
如果不想保留时间戳,配置如下:
1 2 3 4 <commentGenerator > <property name ="suppressDate" value ="true" /> </commentGenerator >
默认生成的注释是不会有数据库表中字段的注释,如果想添加,配置如下
1 2 3 4 <commentGenerator > <property name ="addRemarkComments" value ="true" /> </commentGenerator >
(前提是数据库中对应表的字段,已经添加了注释)
如果不想要注释,配置如下:
1 2 3 4 <commentGenerator > <property name ="suppressAllComments" value ="true" /> </commentGenerator >
jdbcConnection
<jdbcConnection>
,数据库的连接信息。
1 2 3 4 5 6 <jdbcConnection driverClass ="com.mysql.cj.jdbc.Driver" connectionURL ="jdbc:mysql://127.0.0.1:3306/test_mybatis" userId ="root" password ="MySQL@2022" > </jdbcConnection >
javaTypeResolver
<javaTypeResolver>
,配置数据库字段与Java的类型转换规则。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <javaTypeResolver > <property name ="forceBigDecimals" value ="true" /> <property name ="useJSR310Types" value ="true" /> </javaTypeResolver >
javaModelGenerator
<javaModelGenerator>
,javaBean的生成策略,例如
1 2 3 4 5 6 7 <javaModelGenerator targetPackage ="com.kakawanyifan.pojo" targetProject ="./src/main/java" > </javaModelGenerator >
targetPackage
,生成目标包。
targetProject
,生成目标目录。
当trimStrings
设置为true
时的set方法。
1 2 3 public void setPassword (String password) { this .password = password == null ? null : password.trim(); }
不设置trimStrings
时的set方法。
1 2 3 public void setPassword (String password) { this .password = password; }
注意!在MacOS或Linux中,文件目录都是使用斜杆的/
, 而Windows是使用反斜杠\
。下文同样需要注意。
sqlMapGenerator
,SQL映射文件的生成策略。
1 2 3 4 <sqlMapGenerator targetPackage ="com.kakawanyifan.mapper" targetProject ="./src/main/resources" > </sqlMapGenerator >
javaClientGenerator
<javaClientGenerator>
,Mapper接口的生成策略。
1 2 3 4 5 <javaClientGenerator type ="XMLMAPPER" targetPackage ="com.kakawanyifan.mapper" targetProject ="./src/main/java" > </javaClientGenerator >
type
属性有两种取值
XMLMAPPER
:将接口的实现放在SQL映射文件中。
ANNOTATEDMAPPER
,接口的实现通过注解写在接口代码的上方.
table
<table>
,逆向分析的表,tableName
表名,domainObjectName
实体类的名字。
1 2 3 4 <table tableName ="dept" domainObjectName ="Dept" /> <table tableName ="emp" domainObjectName ="Emp" /> <table tableName ="salary" domainObjectName ="Salary" /> <table tableName ="user" domainObjectName ="User" />
执行
执行如图所示之处,运行generate
。
运行结果:
在运行之后,我们还可以找几个方法试一下(该部分内容略,参考我们上文的讨论)。
targetRuntime-MyBatis3
我们先把之前生成的都删除,然后修改generatorConfig.xml
文件的<context>
标签的targetRuntime
属性的值为MyBatis3
,重新生成一下,会得到如下的内容。
示例代码:
1 2 3 4 5 6 7 8 System.out.println(empMapper.selectByExample(null )); EmpExample empExample = new EmpExample(); empExample.createCriteria().andNameEqualTo("Tom" ).andIdBetween(0 ,100 ); empExample.or().andNameEqualTo("李四" ); System.out.println(empMapper.selectByExample(empExample));
运行结果:
1 2 3 4 5 6 7 8 DEBUG 06-17 12:56:23,948 ==> Preparing: select id, name, dept_id from emp (BaseJdbcLogger.java:137) DEBUG 06-17 12:56:23,977 ==> Parameters: (BaseJdbcLogger.java:137) DEBUG 06-17 12:56:24,037 <== Total: 5 (BaseJdbcLogger.java:137) [Emp{id=2, name='李四', deptId=1}, Emp{id=3, name='王五', deptId=1}, Emp{id=4, name='Tom', deptId=2}, Emp{id=5, name='Jerry', deptId=2}, Emp{id=6, name='Lucy', deptId=2}] DEBUG 06-17 12:56:24,049 ==> Preparing: select id, name, dept_id from emp WHERE ( name = ? and id between ? and ? ) or( name = ? ) (BaseJdbcLogger.java:137) DEBUG 06-17 12:56:24,050 ==> Parameters: Tom(String), 0(Integer), 100(Integer), 李四(String) (BaseJdbcLogger.java:137) DEBUG 06-17 12:56:24,052 <== Total: 2 (BaseJdbcLogger.java:137) [Emp{id=2, name='李四', deptId=1}, Emp{id=4, name='Tom', deptId=2}]
解释说明:
selectByExample
:按条件查询,需要传入一个example对象或者null;如果传入一个null,则表示没有条件,也就是查询所有数据。
example.createCriteria().xxx
:创建条件对象,通过andXXX方法为SQL添加查询添加,每个条件之间是and关系。
example.or().xxx
:将之前添加的条件通过or拼接其他条件。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 empMapper.updateByPrimaryKey(new Emp(3 ,"张三三" ,null )); empMapper.updateByPrimaryKeySelective(new Emp(3 ,"张三三" ,null )); EmpExample empExample = new EmpExample(); empExample.createCriteria().andIdEqualTo(3 ); empMapper.updateByExample(new Emp(null ,"张三三" ,null ),empExample); empExample.clear(); empExample.createCriteria().andDeptIdBetween(0 ,100 ); empMapper.updateByExampleSelective(new Emp(null ,"张三三" ,null ),empExample);
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 DEBUG 06-17 13:20:52,099 ==> Preparing: update emp set name = ?, dept_id = ? where id = ? (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,119 ==> Parameters: 张三三(String), null, 3(Integer) (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,122 <== Updates: 1 (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,137 ==> Preparing: update emp SET name = ? where id = ? (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,137 ==> Parameters: 张三三(String), 3(Integer) (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,138 <== Updates: 1 (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,148 ==> Preparing: update emp SET name = ? WHERE ( id = ? ) (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,148 ==> Parameters: 张三三(String), 3(Integer) (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,150 <== Updates: 1 (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,151 ==> Preparing: update emp SET name = ? WHERE ( dept_id between ? and ? ) (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,151 ==> Parameters: 张三三(String), 0(Integer), 100(Integer) (BaseJdbcLogger.java:137) DEBUG 06-17 13:20:52,152 <== Updates: 4 (BaseJdbcLogger.java:137)
解释说明:
updateByPrimaryKey
:通过主键进行数据修改,如果某一个值为null,也会将对应的字段改为null
updateByPrimaryKeySelective()
:通过主键进行选择性数据修改,如果某个值为null,则不修改这个字段
updateByExample
和updateByExampleSelective
类似。
分页插件
依赖
1 2 3 4 5 6 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper</artifactId > <version > 5.2.0</version > </dependency >
配置
在MyBatis的配置文件(mybatis-config.xml)中配置插件。
1 2 3 4 <plugins > <plugin interceptor ="com.github.pagehelper.PageInterceptor" > </plugin > </plugins >
使用
在查询之前使用PageHelper.startPage(int pageNum, int pageSize)
开启分页
pageNum
:当前页的页码
pageSize
:每页显示的条数
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 PageHelper.startPage(2 ,4 ); UserExample userExample = new UserExample(); userExample.setOrderByClause("id" ); List<User> userList = userMapper.selectByExample(userExample); PageInfo<User> userPgae = new PageInfo<>(userList); System.out.println("每一页的记录" ); userPgae.getList().forEach(user -> System.out.println(user)); System.out.println("当前页的页码 " + userPgae.getPageNum()); System.out.println("每页的记录数 " + userPgae.getPageSize()); System.out.println("当前页的实际记录数 " + userPgae.getSize()); System.out.println("总记录数 " + userPgae.getTotal()); System.out.println("总页数 " + userPgae.getPages());
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 DEBUG 06-18 11:28:48,051 Cache Hit Ratio [SQL_CACHE]: 0.0 (LoggingCache.java:60) DEBUG 06-18 11:28:48,123 ==> Preparing: SELECT count(0) FROM user (BaseJdbcLogger.java:137) DEBUG 06-18 11:28:48,147 ==> Parameters: (BaseJdbcLogger.java:137) DEBUG 06-18 11:28:48,168 <== Total: 1 (BaseJdbcLogger.java:137) DEBUG 06-18 11:28:48,170 ==> Preparing: select id, username, password, gender, addr from user order by id LIMIT ?, ? (BaseJdbcLogger.java:137) DEBUG 06-18 11:28:48,171 ==> Parameters: 4(Long), 4(Integer) (BaseJdbcLogger.java:137) DEBUG 06-18 11:28:48,174 <== Total: 4 (BaseJdbcLogger.java:137) 每一页的记录 {id=15, username='张三三', password='123456', gender='男', addr='某地'} {id=16, username='张三三', password='123456', gender='男', addr='某地'} {id=17, username='张三三', password='123456', gender='男', addr='某地'} {id=18, username='张三三', password='123456', gender='男', addr='某地'} 当前页的页码 2 每页的记录数 4 当前页的实际记录数 4 总记录数 24 总页数 6
常用数据:
每一页的记录:userPgae.getList()
当前页的页码:userPgae.getPageNum()
每页的记录数:userPgae.getPageSize()
当前页的实际记录数:userPgae.getSize()
总记录数:userPgae.getTotal()
总页数:userPgae.getPages()