MyBatis 最初是 Apache 的一个开源项目 iBatis,10 年由 Apache Software Foundation 迁移到 Google Code,iBatis3.x 正式更名为 MyBatis,13 年 11 月迁移到 GitHub
MyBatis 是一个基于 Java 的持久层框架。提供的持久层框架包括 SQL Maps 和 Data Access Object(DAO)
<groupId>org.examplegroupId>
<artifactId>Mybatis_demo1artifactId>
<version>1.0-SNAPSHOTversion>
<packaging>jarpackaging>
<build>
<finalName>SpringbootMybatisfinalName>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-site-pluginartifactId>
<version>3.7.1version>
plugin>
plugins>
build>
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.7version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.3version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>RELEASEversion>
<scope>compilescope>
dependency>
dependencies>
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/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
dataSource>
environment>
environments>
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
mappers>
configuration>
Mapper 接口主要用来写操作数据库数据的方法
创建个 t_user 表:
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NULL DEFAULT NULL,
`password` varchar(20) NULL DEFAULT NULL,
`age` int(11) NULL DEFAULT NULL,
`sex` char(1) NULL DEFAULT NULL,
`email` varchar(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
)
对应创建个实体类,在 src/main/java 下新建个 com.mybatis.pojo 包存放 User 类
@Data
@AllArgsConstructor
public class User {
private Integer id;
private String ysername;
private String paassword;
private Integer age;
private String sex;
private String email;
}
相应的,pojo 同级目录创建 mapper 包,其中创建 UserMapper 接口
public interface UserMapper {
/**
* 添加用户信息
* @return 添加结果
*/
int insertUser();
}
每个 mapper 接口对应一个操作数据库的 xml 映射文件,以下为 UserMapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.mapper.UserMapper">
<insert id="insertUser">
insert into t_user values(null,'张三','123',23,'女','[email protected]')
insert>
mapper>
最后在 mybatis 的配置文件引入你写的映射文件也就是上面的:
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
mappers>
此时数据库的 t_user表对应 User 实体类,User 实体类有它的数据操作接口 UserMapper,SQL 语句不可能写在 java 文件中,所以有映射文件 UserMapper.xml,此时就有:数据库表->实体类->接口->接口映射文件,打通了与数据库的连接。
此处我的包名命名并不规范,应该为com.企业名.项目名.模块名,比如导入 MyBatis 某个类时你会是 import org(非盈利组织).apache(apache公司).ibatis(ibatis项目).io(输入输出流功能模块).*
那么第一步加载配置文件,通过 Resources 类的静态方法 getResourceAsStream 来读取配置文件获取对应的字节输入流,还有其他的以文件形式之类的来获取,那么此时根据配置类文件名来获取
InputStream is = Resources.getResourceAsStream("mybatis-config.xml")
然后获取SqlSessionFactoryBuilder对象,这是提供 sqlSession 工厂对象的构建对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder()
Session:代表 java 与数据库之间的会话,例如 HttpSession 就是 java 和浏览器之间的会话
SqlSessionFactory:生产 SqlSession 的工厂
然后获取 SqlSessionFactory,通过 SqlSessionFactory 的构建对象来生产 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is)
然后获取 SqlSession,通过Session工厂对象也就是 SqlSessionFactory 来打开 SqlSession
SqlSession SqlSession = sqlSessionFactory.openSession();
然后获取接口对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class)
测试一下我们之前在 UserMapper.xml 中写的 Sql 语句的添加功能(方法底层使用了代理模式)
int result = mapper.insertUser()
由于我们在配置文件中写了事物管理方式是最原始的 JDBC 方式,所以需要手动提交或回滚事务,在这里使用 SqlSession 提交事务
sqlSession.commit()
如果插入的中文数据乱码了,那么修改数据库配置的 url 的编码方式,即将 mybatis-config.xml 中的 url 修改为:
上述添加测试功能优化
SqlSession默认不自动提交事务,若需自动提交可以 SqlSessionFactory.openSession(true)
添加日志管理,输出事务中的 sql 语句:
添加 log4j 依赖:
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
resource 下加入 log4j.xml(log4j 配置文件)
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>
日志的级别:FATAL(致命)>ERROR(错误)>WARN(警告)>INFO(信息)>DEBUG(调试)
从左到右打印的内容越来越详细
找到 sql 语句的依据是根据(全名类 + 方法名)或者说 (命名空间 + sqlid)
但是如果像之前的增删改一样的步骤,先在 mapper 文件写方法,然后在映射文件 mapper.xml 写 sql 如下,这样是会报错的
<select id="selectUserById">
select * from t_user where id = 4
select>
为什么?因为增删改操作的返回值可以是受影响行数,但是查询呢?你要对应实体对象,对应实体对象列表,亦或是某个字段的值?人家怎么知道你要什么,所以你需要设置查询结果,resultType 或者 resultMap。
修改上面的 sql 映射部分
<select id="selectUserById" resultType="com.mybatis.pojo.User">
select * from t_user where id = 4
select>
resultType:默认映射类型,就是查询到的结果,根据字段名和属性名的对应关系,将字段的值赋给映射类型的属性,也就是表字段名和对象属性名尽量保持一致
resultMap:自定义映射:字段名和属性名不一致、多对一、一对多时就需要用这个了
属性:
上面的数据库配置属性都写死了,但是其实我们是会写在 properties 中
resource 下创建 jdbc.properties
driver=com.mysql.jdbc.Driver
//这里注意将 '&' 重新改为 '&'
url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
username=root
password=123456
但是这样键值对的形式,比如说 password,可能会重名别人怎么知道访哪个配置个文件的哪个 username,所以我们根据文件名加个前缀,这样就规范多了
jdbc.driver=com.mysql.jdbc.Driver"
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
jdbc.username=root
jdbc.password=123456
那么 mybatis 的配置文件怎么访问你写的 properties 文件呢,当然是引入了
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties" />
<environments default="development">
...
environments>
<mappers>
......
mappers>
configuration>
再用 ${} 的形式来读取 properties 中的值
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties" />
<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>
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
mappers>
configuration>
你每个查询的方法的结果集都要写全类名的话,确实很累,所以你可以为结果类型起别名
修改 mybatis-config.xml
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties" />
<typeAliases>
<typeAlias type="com.mybatis.pojo.User" alias="User">typeAlias>
typeAliases>
<environments default="development"/>
<mappers/>
configuration>
我们习惯性把新加的标签放在最下面或最上面为什么我把 typeAliases 放在了中间呢,当我放在最上面时报错了:
The content of element type “configuration” must match “(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)”.
什么意思?configuration 中的内容必须按照它括号内的顺序来填写,没有则跳过。对应到这里就是properties 之后才能放 typeAliases
此时把 UserMapper.xml 中的查询结果修改依旧能正常查询
<select id="getAllUser" resultType="User">
select * from t_user
select>
这里大小写的 User 都能找到,你写成 UsEr 都没事,小细节吧算是
而且 typeAlias 中的 type 必写,但是 alias 非必写,写了就只能按照你起的别名来找,而不写则默认根据类名来设置别名也就是和你设置的 User 相同,这里同样都不区分大小写
但是你是否会感觉还是麻烦,还得以类为单位设置别名,那么你可以以包为单位设置别名
<typeAliases>
<package name="com.mybatis.pojo">package>
typeAliases>
我们知道一张表对应一个实体类,一个实体类对应一个 Mapper ,一个 Mapper 对应一个 xml 映射文件,那么映射文件多的时候,MyBatis 配置文件岂不是要引入多次映射文件,所以你可以在 mybatis-config.xml 直接引入 映射文件所在包
但是以包为单位引入映射文件需要符合两个条件:
两种方式:${} 和 ${}
先新建一个模块 MyBatis_demo2
那么还是一样的步骤:pom.xml 导入依赖 -> 新建 mybatis-config.xml
那么其实每次都是一样的操作,所以你可以配置 mybatis-config.xml 模板,省得每次都复制粘贴
Settings–>Editor–>File and Code Templates:File 中新增 name 为 mybatis-config,Extension 为 xml 内容如下的模板文件
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"/>
<typeAliases>
<package name=""/>
typeAliases>
<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>
<mappers>
<package name=""/>
mappers>
configuration>
我们再创建 Mapper 接口,映射文件,映射文件也搞个代码模板吧
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">
mapper>
然后编写测试类,这时我们发现,我又要重复获取 SqlSession 然后获取 mapper 调用方法的操作,那就封装成自己的工作类吧:
新建 pojo 同级目录 utils,创建文件 SqlSessionUtils.java
public class SqlSessionUtils {
public static SqlSession getSqlSession(){
SqlSession sqlSession = null;
//这里直接 try/catch 省的到时候调用的地方还要处理异常
try {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
sqlSession = sqlSessionFactory.openSession(true);
} catch (IOException e) {
e.printStackTrace();
}
return sqlSession;
}
}
测试类如下:
public class ParameterMapperTest {
@Test
public void testGetAllUser(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
List<User> list = mapper.getAllUser();
list.forEach(n-> System.out.println(n));
}
}
测试有警告,把 log4j 配置文件复制过来就 ok
尝试直接返回 mapper 接口:
public class SqlSessionUtils {
public static <T> T getMapper(Class mapperClass){
T mapper = null;
//这里直接 try/catch 省的到时候调用的地方还要处理异常
try {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
mapper = (T) sqlSession.getMapper(mapperClass);
} catch (IOException e) {
e.printStackTrace();
}
return mapper;
}
}
测试类:
public class ParameterMapperTest {
@Test
public void testGetAllUser(){
ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class);
List<User> list = mapper.getAllUser();
list.forEach(n-> System.out.println(n));
}
}
字符串拼接的方式:
@Test
public void testJDBC() throws Exception {
String userName = "admin";
Class.forName("");
Connection connection = DriverManager.getConnection("","","");
PreparedStatement ps = connection.prepareStatement("select * from t_user where username='"+userName+"'");
}
我们可以看到这样不仅拼接起来很麻烦,你得手动加单引号,还容易 SQL 注入
简单插一嘴 SQL 注入
例如你的接口中调用登录方法时拼接的 SQL 为:
String sql = "select * from user_table where username='" + userName + "' and password='"+ password +"'";
如果我们传入的 username 为:' or 1 = 1 --
,sql 语句就变成了:
select * from user_table where username=’ ’ or 1 = 1 – ‘and password=’ ’
--
意味着注释,他后面的语句就被注释了
所以你的 sql 实际上是:
select * from user_table where username=’ ’ or 1 = 1
这意味着你的登录时对用户名和密码的校验没有任何作用,谁都能登录
如果换个更加狠的传个删除库表的 sql:
select * from user_table where username=’ ’ ;DROP DATABASE (DB Name) --’ and password=’ ’
那你等着被开除吧
所以一般用另一种方式,占位符:
@Test
public void testJDBC() throws Exception {
String userName = "admin";
Class.forName("");
Connection connection = DriverManager.getConnection("","","");
PreparedStatement ps = connection.prepareStatement("select * from t_user where username = ?");
ps.setString(1,userName);//字符类型所以用 setString,将第 1 个占位符的内容替换为 username
}
这种方式就无需手动写单引号,也不用担心 SQL 注入
${ }:本质就是字符串拼接
#{ }:本质就是占位符赋值
那么我们肯定能用 # 就尽量不用 $,因为便捷且不用担心 SQL 注入,但是有些 SQL 必须使用 $
那我可能传入一个字面量,多个字面量,集合…,所以现在来考虑一下不同参数的情况下如何获取参数
//ParametetMapper.java:
User getUserByUsername(String username);
//映射文件:
<select id="getUserByUsername" resultType="User">
select * from t_user where username = #{username}
</select>
//测试类:
@Test
public void testGetUserByUsername(){
ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class);
User user = mapper.getUserByUsername("张三");
System.out.println(user);
}
select * from t_user where username = '${username}'
//ParametetMapper.java:
User checkLogin(String username,String password);
//映射文件:
<select id="checkLogin" resultType="User">
select * from t_user where username = #{username} and password = #{password}
</select>
//测试类:
@Test
public void testCheckLogin(){
ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class);
User user = mapper.checkLogin("张三","123");
System.out.println(user);
}
发现报错了,根本解析不了 sql ,传过去的参数都获取不到
根据报错时的解决方案可知可用参数有 {arg0, arg1, param1, param2}
MyBatis 在接收到多个参数时会自动放入 map 集合,以如上两种方式为键,参数为值,所以 sql 修改如下
select * from t_user where username = #{arg0} and password = #{arg1}
那么根据原理你还能这么写:
select * from t_user where username = #{arg0} and password = #{param2}
换用 ${} 方式还是一样,注意单引号就 ok
既然 MyBatis 在接收多个参数时自动放入 map,那索性我自己用 map 装起来参数,传参改为 map
//ParametetMapper.java:
User checkLoginByMap(Map<String,Object> map);
//映射文件:
<select id="checkLoginByMap" resultType="User">
select * from t_user where username = #{username} and password = #{password}
</select>
//测试类:
@Test
public void testCheckLoginByMap(){
ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("username","张三");
map.put("password","123");
User user = mapper.checkLoginByMap(map);
System.out.println(user);
}
传参 map 我们自己设定了 key,所以映射文件中的 sql 获取参数方式就根据我们设定的 key 来获取
用 ${} 依旧一样注意单引号就 ok(还是那句话,如果你不怕 SQL 注入就用)
其实 map 都知道根据 key 取值了,那么对象肯定也就是根据属性来获取属性值了
这里有个知识点:什么叫属性?你会认为是成员方法,你可以这么认为,但是属性其实是 get、set 方法中去掉 get、set 后获取的字符串的首字母大写改为小写。例如 setName,name 就是属性。因为有时候没有相对应的成员变量,却有相对应的 get,set 方法。
//ParametetMapper.java:
int insertUser(User user);
//映射文件:
<insert id="insertUser">
insert into t_user values (null,#{username},#{password},#{age},#{sex},#{email})
</insert>
//测试类:
@Test
public void testInsertUser(){
ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class);
User user = new User(null,"王五","123",28,"男","[email protected]");
int result = mapper.insertUser(user);
System.out.println(result);
}
最后还是一样的道理,${} 注意单引号
第二种方式明显那个键名不符合我们的规范,但是第三种方式我们自己建 map 好像也很费力,所以我们一般都会采用命名参数的方式
//ParametetMapper.java:
User checkLoginByParam(@Param("username") String username, @Param("password") String password);
//映射文件:
<select id="checkLoginByParam" resultType="com.mybatis.pojo.User">
select * from t_user where username = #{username} and password = #{password}
</select>
//测试类:
@Test
public void testCheckLoginByParam(){
ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class);
User user = mapper.checkLoginByParam("张三","123");
System.out.println(user);
}
加了 @Param 注解的参数,到时候存到 MyBatia 的 map 中时就会根据你写的 @Param 的值来存储对应的值。你也可以故意写错 sql 语句中 #{} 的值来看看可用的值有哪些,会发现有 password,param1,usernam,param2
总结:建议将以上的两种情况都统一成两种情况:@Param 或者实体类,除非你说你只能传 map。
try {
//Object的equals看你是不是同一个对象,一个是Object.class,是 class java.lang.Object,另一个是method.getDeclaringClass(),是 interface com.mybatis.mapper.ParameterMapper,怎么都不可能相等,所以进 this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession)
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
//没啥好说的直接进 excute(sqlSession, args)
return this.mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
//commond 有 name、type 两个属性,name 为全类名,而类型就是映射文件中你写的 SELECT,所以进 case SELECT
switch(this.command.getType()) {
case INSERT:
...
case UPDATE:
...
case DELETE:
...
//来这
case SELECT:
//我们定义的方法也就是 method,返回结果是 User,所以以下的都不是,直接进 else,
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
//来这,重头戏来了,进 this.method.convertArgsToSqlCommandParam(args)
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
...
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
public Object convertArgsToSqlCommandParam(Object[] args) {
return this.paramNameResolver.getNamedParams(args);
}
public Object getNamedParams(Object[] args) {
//name 是啥?怎么拿的 name 参考下面的代码解读
int paramCount = this.names.size();
if (args != null && paramCount != 0) {
//下面代码可知 this.hasParamAnnotation 为 true
if (!this.hasParamAnnotation && paramCount == 1) {
...
} else {
Map<String, Object> param = new ParamMap();
int i = 0;
for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
//结合上下代码现在是两个数组
//args:{"张三","123"}
//names:{"username","password"}
Entry<Integer, String> entry = (Entry)var5.next();
//这可不就是 param.put("username","张三")
param.put(entry.getValue(), args[(Integer)entry.getKey()]);
//这里就是默认为我们设置的键值对 param1,param2......
String genericParamName = "param" + (i + 1);
//如果我们没用 @Param 注解设置 key 为 paramx,我们当然不会这么做
if (!this.names.containsValue(genericParamName)) {
//默认生成的键值对也放进去了
param.put(genericParamName, args[(Integer)entry.getKey()]);
}
}
//这不就是最后的{"username":"张三","param1":"张 三","password":"123",
// "param2":"123"}
return param;
}
} else {
return null;
}
}
name 的来源:
public ParamNameResolver(Configuration config, Method method) {
this.useActualParamName = config.isUseActualParamName();
Class<?>[] paramTypes = method.getParameterTypes();
//获取参数注解,可能多个参数有多个注解所以这里使用二维数组
Annotation[][] paramAnnotations = method.getParameterAnnotations();
SortedMap<Integer, String> map = new TreeMap();
//这是 Annotation[] 的长度也就是列的长度也就是有注解的参数的个数
int paramCount = paramAnnotations.length;
for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) {
//如果不是特殊的参数类型注解,显然不是
if (!isSpecialParameter(paramTypes[paramIndex])) {
String name = null;
//获取第 paramIndex 个参数的所有注解
Annotation[] var9 = paramAnnotations[paramIndex];
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
Annotation annotation = var9[var11];
//由于我们有 @Param 注解所以这里是 true
if (annotation instanceof Param) {
//这个判断有无 Param 注解的标志就变成 true
this.hasParamAnnotation = true;
//然后获取注解的值例如@param("username"),这个 value 就是 username
name = ((Param)annotation).value();
break;
}
}
if (name == null) {
...
}
//所以第一次循环就有了{0:"username"},以此类推
map.put(paramIndex, name);
}
}
//最后 names 就是把我们得到的 map 转换一成集合{"username","password"}
this.names = Collections.unmodifiableSortedMap(map);
}
例如查询用户数
<select id="getCount" resultType="java.lang.Integer">
select count(*) from t_user
select>
这里结果集设置成 Int,integer 发现都可以,这是因为 MyBatis 自动为我们设置了一些默认的类型别名,且不区分大小写
<select id="getUserByIdToMap" resultType="Map">
select * from t_user where id = #{id}
select>
@Test
public void testGetUserByIdToMap(){
SelectMapper mapper = SqlSessionUtils.getMapper(SelectMapper.class);
Map<String, Object> map = mapper.getUserByIdToMap(4);
System.out.println(map);
}
我们知道 map 是以键值对的形式存储数据,表中数据正好以字段名为 key 以字段值为 value
很像,多个用户用用户类型的 list 接收,所以用 map 类型接收也得用 list
<!--List
用 list 接收是因为查询到每条数据都被自动被转为 map,根据字段名和字段的值,我们形成不了一个完整的数据。但是其实我们的 map 是可以存储多条数据的,因为 Object 可以是一个 User 而不一定要是一个字符串类型的某个字段的值。这时你可以在 mapper 接口中的方法上加上 MapKey 注解,为查询到的每条数据设置 key,这样就可以用 map 根据你设置的 key 存储多条数据
总结:查询多条数据时
老三样,Mapper、Mapperxml、Test
如果用 #{} 根据用户名模糊查询
<select id="getUserByLike" resultType="User">
select * from t_user where username like '%#{username}%'
select>
那么测试时会发现报错了,因为你用比如 #{username} ,最后会被解析成 ‘xx’,那么你的 SQL 语句就变成了
select * from t_user where username like '%'xx'%'
,所以会报错
修改 sql 如下
select * from t_user where username like concat('%',#{username},'%')
concat 会将多个字符串拼接成一个字符串,其实就是字符串相加,所以如下
concat('%',#{username},'%')='%' + '张' + '%'='%张%'
最后的 sql 就拼接成: select * from t_user where username like '%张%'
既然 concat 函数可以,那么自己拼接字符串当然也行
这里注意字符串用双引号:
select * from t_user where username like "%"#{username}"%"
感觉貌似是会自动转换成像 concat 一样的字符串相加
例如 delete * from t_user where id in {1,2,3}
那么我们的传参很明显应该是一个字符串,例如 “1,2,3”
<delete id="deleteMore">
delete from t_user where id in (${ids})
delete>
这里还想用 #{} 括号里面就变成了 ‘1,2,3’ ,谁的 id 会等于 ‘1,2,3’
例如查询指定表中的数据,这时表名就需要动态设置
我们知道表名不能带有单引号,所以只能用 ${}
例如我们有一个操作界面,可以新建班级的同时把勾选的同学也添加进班级,也就是说完成这个操作会执行两步,第一步,新建班级插入班级表,第二步,把勾选的每个学生的班级 id 置为新增的班级的 id,那我刚生成的 id 我怎么获取到?
@Test
public void testJDBC() throws Exception {
Class.forName("");
Connection connection = DriverManager.getConnection("", "", "");
//默认不返回,所以传了个参数为返回
PreparedStatement ps = connection.prepareStatement("insert into t_user values(null,?,?,?,?)",Statement.RETURN_GENERATED_KEYS);
ps.executeUpdate();
ResultSet resultSet = ps.getGeneratedKeys();
}
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values(null,#{username},#{password},#{age},#{sex},#{email})
insert>
@Test
public void testInsertUser() throws Exception{
SQLMapper mapper = SqlSessionUtils.getMapper(SQLMapper.class);
User user = new User(null,"王六","123",20,"女","[email protected]");
mapper.insertUser(user);
System.out.println(user);
}
实际开发中我们数据库表的字段不一定和实体类属性一致,例如 User 的 user_name 和 User 类 的userName,还有一对多等情况。我们在之前结果集设置都用 resultType,此时显然不适用,需要自定义映射 resultMap
建个新模块 mybatis_demo3,pom.xml 添加依赖,jdbc 属性文件,log4j 配置文件,mybatis 配置文件,建mapper、pojo 包,建对应 mapper 目录,创建实体类,映射文件,测试类
新建 t_emp 以及 t_dept,也就是员工表和部门表,对应实体类如下
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
//员工类
public class Emp {
private Integer eid;
private String empName;
private Integer age;
private Dept dept;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
//部门类
public class Dept {
private Integer did;
private String deptName;
private List<Emp> emps;
}
新建查询所有员工的方法,如果还使用 restltType
<select id="getAllEmp" resultType="com.mybatis.pojo.Emp">
select * from t_emp
select>
测试打印发现因为 emp_name 和 empName 不同名所以无法获取到值,其他对的上的可以获取到
既然名字对应不上,那我就让你对应上,当然我不可能改变字段名或者属性名
select eid,emp_name empName,age from t_emp
既然数据库字段以及实体类属性名各有命名规范,那么 MyBatis 也意识到了这点,可以在 MyBatis 配置文件中设置
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
settings>
上面两种方法一种太麻烦,而且不易维护,另一种只能解决默认的下划线转驼峰的情况,还是有局限性,其实用 resultMap 解决当前问题也是大材小用,一般都是解决一对多,多对一的问题,以下只是演示用法
<select id="getAllEmp" resultMap="">
select * from t_emp
select>
那 resultMap 的值又是什么,既然是自定义映射关系,那么你肯定会定义一个 resultMap,而定义时取的名字就是我们要填入的值,这样我们就通过这个名字对应你定义的 resultMap
<resultMap id="empResult" type="emp">
<id property="eid" column="eid"/>
<result property="age" column="age"/>
<result property="empName" column="emp_name"/>
resultMap>
<select id="getAllEmp" resultMap="empResult">
select * from t_emp
select>
例如多个员工对应一个部门,那么我们就在多的一方设置一的属性,也就是说,Emp 类中加入属性
private Dept dept;
尝试查询员工以及员工对应部门的信息
我们这里结果集用 resultType 肯定不行,因为你查出来的什么能对应 Dept 类型的字段呢,所以只能用 resultMap
测试查询:select * from t_emp left join t_dept on t_emp.did = t_dept.did where t_emp.eid = 1
结果如下
eid | emp_name | age | did | did | dept_name |
---|---|---|---|---|---|
1 | 张三 | 10 | 1 | 1 | A |
那么其实我们就是要把查出来的 eid、emp_name、age 给 emp 的三个属性,把 did 和 dept_name 给 emp 的 dept 属性
<resultMap id="empAndDeptResultOne" type="emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<result property="dept.did" column="did"/>
<result property="dept.deptName" column="dept_name"/>
resultMap>
<select id="getEmpAndDept" resultMap="empAndDeptResultOne">
select * from t_emp left join t_dept on t_emp.did = t_dept.did where t_emp.eid = #{eid}
select>
<resultMap id="empAndDeptResultTwo" type="emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<association property="dept" javaType="dept">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
association>
resultMap>
,说实话这个标签的属性真的很见名知义,然后我们在其中继续按照一个实体类来处理,有 id、result我们可以一步到位查出来,那我们可以分成多步来查询啊,我们查出 emp 也就得到了 did,我们再通过 did查出 dept 最后把结果赋值给 emp 不就好了
首先便是第一步:
<resultMap id="empAndDeptByStepOneResult" type="emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<association property="dept" select="com.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo" column="did"/>
resultMap>
<select id="getEmpAndDeptByStepOne" resultMap="empAndDeptByStepOneResult">
select * from t_emp where eid = #{eid}
select>
我们这里依旧使用 association,但是里面不再是属性名 property 以及对应的 java 类 javaType,而是属性名 property 以及下一步的 sql 也就是 select,通过 select 的唯一标识,还有就是传给下一步的参数 column,这里的 column 不是映射的字段名而是下一步中所要使用的这一步传过去的条件,例如我们先查询到 did 然后以此为依据查询进行下一步查询,这里注意 select 的唯一标识是全类名加方法名,因为不同映射文件中的 select 也能有相同的 id,所以带上全类名就能确保唯一性
既然获得了 did ,那么就可以查询部门信息了
<select id="getEmpAndDeptByStepTwo" resultType="dept">
select * from t_dept where did = #{did}
select>
这里的 did 就是第一步提供的 column 中的 did,而这里查询出的结果就根据 select 的唯一标识返回给第一步,这也就实现了查询出实体类赋值给某个实体类的实体类属性,也就是查询出 Dept 类数据赋值给 Emp 类的 dept 字段
分布查询的好处就是可以实现延迟加载,默认不开启。我们在第一步的 resultMap 中配置了第二步的 sql,也就是说默认当你调用 getEmpAndDeptByStepOne 时,getEmpAndDeptByStepTwo 也会随之调用,但是当你开启后就会按需调用了。
MyBatis 配置文件设置 lazyLoadingEnabled 为 true 即可
开启延迟加载后我们访问什么信息就执行相关的什么 sql,例如测试类如下
@Test
public void testGetEmpAndDeptByStep(){
EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class);
Emp emp = mapper.getEmpAndDeptByStepOne(1);
//System.out.println(emp);
System.out.println(emp.getEmpName);
}
我们将打印内容换成员工名,我们会发现控制台打印的 sql 中只执行了查询员工的 sql 而没有执行查询部门信息的 sql 了,换成打印部门信息同理
但是开启延迟加载后会应用到全局,我们有些 sql 不希望延迟加载怎么办?用 fetchType
<association
property="dept"
select="com.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo"
column="did"
fetchTyper="eager"/>
resultMap>
第一步中的 resultMap 中配置的分步查询中设置 fetchType 属性为 eager 就可以开启立即加载,设置为 lazy 则还是延迟加载(注意要先开启延迟加载)
例如一个部门对应多个员工,那么我们就在“一”的一方定义多的集合属性
Dept 类加入属性 private List
<resultMap id="deptAndEmpResultMap" type="dept">
<id property="did" column="did">id>
<result property="deptName" column="dept_name">result>
<collection property="emps" ofType="emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
collection>
resultMap>
<select id="getDeptAndEmp" resultMap="deptAndEmpResultMap">
select *
from t_dept
left join t_emp on t_dept.did = t_emp.did
where t_dept.did = #{did}
select>
我们在多对一时使用的 resultMap 设置结果集时使用的 association 是对应 Emp 类的 dept 属性,是一个 java 类型所以是 javaType,但是我们在实现一对多的时候,“多”是集合,它对应的属性是一个集合,我们 sql查询的并不是对应集合类型数据而是属性所对应的集合中存储的数据类型,也就是 emps 中储存的 Emp 类型的数据,所以使用的是 ofType
既然一对多可以分步查询,那么对应的多对一自然也可以
还是一样先考虑分成哪几个 sql,分成两步,其中 collection 标签的 property、select、colume 属性,包括延迟加载与多对一都完全一致,不过分赘述
个人感觉:其实一对多与多对一不用去想哪个对应什么。只要想比如一个部门拥有多个员工,你就给部门类 List
本质就是实现 sql 拼接,但是拼接的是条件,比如查一本书可以根据年代、作者、书名,你选了几个条件,对应的 sql 就应该根据几个条件查询。如果你使用 java 语言去拼接 sql,你可以想象有多复杂:先定义字符串 select * from table,除非有条件不为 null 且不等于空你才能拼接上 where,如果有两个及以上条件,第二个条件开始就得是 and …
新建 DynamicMapper 三件套
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp where
<if test="empName !=null and empName != ''">
emp_name = #{empName}
if>
<if test="age !=null and age != ''">
and age = #{age}
if>
select>
@Test
public void testGetEmpByCondition(){
DynamicMapper mapper = SqlUtils.getMapper(DynamicMapper.class);
List<Emp> emps = mapper.getEmpByCondition(new Emp( "张三", 10));
emps.forEach(System.out::println);
}
测试会发现没有任何问题查询到了,但是如果将张三换成空字符串或者 null 呢?sql 语句就变成了
select * from t_emp where and age = 10
很明显 sql 语句就错误了
稍微修改 sql
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp where 1=1
<if test="empName !=null and empName != ''">
and emp_name = #{empName}
if>
<if test="age !=null and age != ''">
and age = #{age}
if>
select>
添加恒成立条件此时不影响结果,但是其他条件就都可以用 and 拼接上,并且如果所有条件都不成立时,我们的 where 也不会多出来
if:根据标签中的 test 属性所对应表达式决定标签中的内容是否需要拼接到 sql 中
修改 sql 如下
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp
<where>
<if test="empName !=null and empName != ''">
and emp_name = #{empName}
if>
<if test="age !=null and age != ''">
and age = #{age}
if>
where>
select>
此时发现,就算 empName 为空也能根据 age 查询到数据,也就是说 MyBatis 帮我们自动去除了 and,同理 or 也能自动去除,如果条件都不成立,即 where 标签中无内容时 where 也会被自动去除
==注意:==where 标签只能去除内容前多余的 and/or,内容后的不能自动去除
那么标签中的内容后面有 and 怎么办
将 trim 标签中的内容前面或后面去掉指定内容
修改 sql 如下
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp
<trim prefix="where" suffixOverrides="and|or">
<if test="empName !=null and empName != ''">
emp_name = #{empName} and
if>
<if test="age !=null and age != ''">
age = #{age} and
if>
trim>
select>
此时会自动拼接上 where,并且去除标签中内容的后面的 and 以及 or,如果 trim 标签中内容为空,trim 也直接不起作用,不会多出个 where
choose 为父标签,when 就相当于 if,else if,else if…,otherwise 相当于 else
新建方法映射 sql 以及测试
<select id="getEmpByChoose" resultType="Emp">
select * from t_emp
<where>
<choose>
<when test="empName != null and empName != ''">
emp_name = #{empName}
when>
<when test="age != null and age != ''">
age = #{age}
when>
<otherwise>
did = 1
otherwise>
choose>
where>
select>
根据 if elseif else 的规则可知,when 标签你起码得有一个吧,但是 otherwise 就不一定要有了,而且根据 if 的规则只要有条件成立就拼接上去了,其他的就不判断了。
当我们进行批量操作的时候,比如批量删除,我们会例如 delete from table where id in (1,2,3),那么这时我们要传入的就是一个数组,那么数组的每个值怎么放进去呢?使用 foreach 标签
首先肯定是写好框架
<delete id="deleteMoreByArray">
delete from t_emp where eid in ()
delete>
那么我们就用 foreach 循环把参数放进去
既然循环,首先肯定有数组 collection,那么数组名我们很自然地会想到用我们传参的名字,有了要循环的数组肯定还要有循环的变量名,就像 for(Interger eid:eids)
,所以有 item 我们就起名为 eid,最后还有一个问题,如果我们自己拼接每个参数之间的逗号,最后的语句一定会多一个逗号,比如:
delete from t_emp where eid in (
<foreach collection="eids" item="eid">
#{eid},
foreach>
)
那么最终结果会如下
delete from t_emp where eid in (1,2,)
所以会有分隔符属性 separator,写成逗号即可
delete from t_emp where eid in (
<foreach collection="eids" item="eid" separator=",">
#{eid}
foreach>
)
测试发现找不到说 eids,可用参数只有 array、arg0,这不禁让我想到之前传两个参数的时候,那么我们同样用 @Param 指定 key 不就好了 int deleteMoreByArray(@Param("eids") Integer[] eids);
测试,大功告成,这也就是之前说的传参除了实体类和 Map,其他都用 @Param
其实 foreach 标签还有两个属性 open、close,这样你连括号也不用写
delete from t_emp where eid in
<foreach collection="eids" item="eid" separator="," open="(" close=")">
#{eid}
foreach>
虽然很笨,但是确实还有一种写法:delete from table where id = 1 or id = 2 or id = 3,但是用了 foreach 标签就不会这么冗长了
根据上面每个属性的讲解不难推测怎么写
delete from t_emp where
<foreach collection="eids" item="eid" separator="or">
eid = #{eid}
foreach>
细心一点的话你可能会想,这里的 separator 的值应该写成 " or ",要注意空格问题,不然 sql 变成了
delete from table where id = 1orid = 2orid = 3
但是其实 MyBatis 会自动在每个参数前后加上空格,所以不用这么写
尝试插入List
到表中,那么 sql 语句会为 insert into t_emp values(xx),(xx)
根据以上的 foreach 标签的使用规则可以推测
<insert id="insertMoreByList">
insert into t_emp values
<foreach collection="emps" item="emp" separator=",">
(null,#{emp.empName},#{emp.age},null)
foreach>
insert>
注意这里就不能用 open 和 close 了,因为这是为标签外全部内容的起始和结束符
我们经常用到一些代码片段,我们就会封装成一个方法,那么对应的,例如我们的查询 sql,最好其实是按需查询,比如 select * from t_emp,我们一般不用查出他的 did,所以可以写成 select eid,emp_name,age from t_emp,但是每次都要写一堆字段太过麻烦,所以我们可以定义成一个 sql 片段
我们随便挑选一个查询 sql
<sql id="empColumns">eid,emp_name,agesql>
select <include refid="empColumns">include> from t_emp
refid 就是你定义的 sql 片段的唯一标识
我们的 sql 进行查询后得到的结果可能会被缓存下来,这样你到时候进行相同的 sql 查询时不需要再访问数据库,直接读取缓存即可
一级缓存是 SqlSession 级别的,通过同一个 SqlSession 查询的数据会被缓存,默认开启
例如新建 CacheMapper 三件套
<select id="getEmpByEid" resultType="Emp">
select * from t_emp where eid = #{eid}
select>
再测试一下
@Test
public void testCache(){
CacheMapper mapper = SqlUtils.getMapper(CacheMapper.class);
Emp emp = mapper.getEmpByEid(1);
System.out.println(emp);
Emp emp2 = mapper.getEmpByEid(1);
System.out.println(emp2);
}
观察控制台会发现打印了一句 sql,也就是说只查询了一次,因为是从同一个 SqlSession 获取的同一个 Mapper 进行的查询
我们再测试用同一个 SqlSession 的不同 mapper 来查询
@Test
public void testCache(){
SqlSession sqlSession = SqlUtils.getSqlSession();
CacheMapper mapper = sqlSession.getMapper(CacheMapper.class);
CacheMapper mapper2 = sqlSession.getMapper(CacheMapper.class);
Emp emp = mapper.getEmpByEid(1);
System.out.println(emp);
Emp emp2 = mapper2.getEmpByEid(1);
System.out.println(emp2);
}
观察控制台发现还是只打印了一句 sql
测试一下不同的 SqlSession
@Test
public void testCache(){
SqlSession sqlSession = SqlUtils.getSqlSession();
SqlSession sqlSession2 = SqlUtils.getSqlSession();
CacheMapper mapper = sqlSession.getMapper(CacheMapper.class);
CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
Emp emp = mapper.getEmpByEid(1);
System.out.println(emp);
Emp emp2 = mapper2.getEmpByEid(1);
System.out.println(emp2);
}
结果证明默认的确是 SqlSession 级别的一级缓存
sqlSession.clearCache();
,自作孽当然不可活)二级缓存是 SqlSesssionFactory 级别,通过同一个 SqlSessionFactory 创建的 SqlSession 查询的结果会被缓存
public class Emp implements Serializable
)此时想要测试,你的工具类肯定要新增一个返回 SqlSessionFactory 的方法,参照之前的方法就行
映射文件添加 cache 标签
<mapper namespace="com.mybatis.mapper.CacheMapper">
<cache/>
...
mapper>
Emp 类实现序列化接口
测试类如下
@Test
public void testTwoCache(){
SqlSessionFactory sqlSessionFactory = SqlUtils.getSqlSessionFactory();
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
System.out.println(mapper1.getEmpByEid(1));
sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
System.out.println(mapper2.getEmpByEid(1));
sqlSession2.commit();
}
控制台有一句 Cache Hit Ratio [com.mybatis.mapper.CacheMapper]: 0.5
就是缓存命中率有 0.5,就是有二级缓存
老实说 SqlSessionFactory.openSession(true)
不是会自动提交事务吗,不加 sqlSession1.commit();
还不行
eviction:缓存回收策略,你不可能无限缓存,所以要进行回收,默认 LRU
LRU(Least Recently Used):最近最少使用原则,也就是移除最长时间不用的
FIFO(First in First out):先进先出,按对象进入缓存的顺序来移除
SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象,内存不足才回收
WEAK(弱引用):移除基于垃圾回收器状态和弱引用规则的对象,垃圾回收时,内存足不足都回收
flushInterval:刷新间隔,单位毫秒,多长时间刷新二级缓存,默认情况不设置,增删改时才刷新
size:引用数目,正整数,可以缓存多少个对象,太大容易内存溢出
readOnly:只读,true/false。
MyBatis 毕竟只是持久层框架,所以缓存技术可能并没有那么好,所以允许调用其他缓存技术的接口,但是只能代替二级缓存
添加依赖
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-ehcacheartifactId>
<version>1.2.1version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
添加配置文件 ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<diskStore path="D:\atguigu\ehcache"/>
<defaultCache maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">defaultCache>
ehcache>
那么怎么让程序知道他该用你添加的 EHCache 呢,通过 cache 标签的 type 属性
存在 SLF4J 时,作为简易日志的 log4j 将失效,此时我们需要借助 SLF4J 的具体实现 logback 来打印日志。
创建 logback.xml
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%npattern>
encoder>
appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
root>
<logger name="com.atguigu.crowd.mapper" level="DEBUG"/>
configuration>
正向工程是框架根据实体类生成数据库表,Hibernate 就支持正向工程
逆向工程则是先创建数据库表,框架负责根据数据库表生成 Java 实体类,Mapper 接口,Mapper 映射文件
逆向工程的本质其实也就是代码生成器
新建一个模块 MyBatis_MBG(MyBatisGegerator)
添加依赖和插件
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.7version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.3version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.0version>
<dependencies>
<dependency>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-coreartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>com.mchangegroupId>
<artifactId>c3p0artifactId>
<version>0.9.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.8version>
dependency>
dependencies>
plugin>
plugins>
build>
到时候可以双击这个插件来生成代码
jdbc.properties 和 log4j.xml 配置文件复制过来,mybatis-config.xml 也生成一下,暂时可以不配置别名包和 mapper 包,生成了再配置
接下来就是最重要的逆向工程的配置文件 generatorConfig.xml,先用一下清新简洁版
DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3Simple">
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis"
userId="root"
password="123456">
jdbcConnection>
<javaModelGenerator targetPackage="com.sky.model"
targetProject=".\src\main\java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
javaModelGenerator>
<sqlMapGenerator targetPackage="com.sky.mapper"
targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true"/>
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.sky.mapper"
targetProject=".\src\main\java">
<property name="enableSubPackages" value="true"/>
javaClientGenerator>
<table tableName="t_emp" domainObjectName="Emp"/>
<table tableName="t_dept" domainObjectName="Dept"/>
context>
generatorConfiguration>
再试一下尊贵奢华版,其实也就是把 targetRuntime 修改一下
<context id="DB2Tables" targetRuntime="MyBatis3">
他生成的实体类会多一个 xxExample,有了这个实体类,就可以进行任意条件的操作了
别忘了 MyBatis 的核心配置 mapper 包目录,实体类重写一下 toString
创个测试类试一下员工姓名等于张三的
@Test
public void testMBG(){
EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class);
EmpExample example = new EmpExample();
example.createCriteria().andEmpNameEqualTo("张三");
List<Emp> list = mapper.selectByExample(example);
list.forEach(System.out::println);
}
这是 QBC (Query By Criteria)风格,也就是条件查询查的时候首先是创建一个条件 createCriteria()
,然后就看你的条件了比如 and emp_name = '张三'
,那就是 andEmpNameEqualTo("张三")
再试一下修改功能,其中有四种修改方式,根据主键或条件修改或选择修改,根据条件或者主键没什么好说的,选择修改又是什么意思
这是我的 t_emp 表的某个员工,id 为 1,姓名为张三,年龄为 10,部门 id 为 1
试一下根据主键修改
@Test
public void testUpdate(){
EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class);
Emp emp = mapper.selectByPrimaryKey(1);
emp.setAge(null);
mapper.updateByPrimaryKey(emp);
}
此时 age 被更新为 null
根据主键选择修改则不同,先把年龄改回 10,再测试选择修改
@Test
public void testUpdate(){
EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class);
Emp emp = mapper.selectByPrimaryKey(1);
emp.setAge(null);
mapper.updateByPrimaryKey(emp);
}
此时发现年龄未被修改,选择修改就是如果传过去的某个属性的值为 null 则跳过该字段不做修改,如果不是选择修改那就是你传什么我就改成什么
添加依赖
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>5.1.2version>
dependency>a
然后在 MyBatis 核心配置文件配置分页插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
plugins>
先回顾一下之前的分页
public class PageHelperTest {
/**
* 分页是根据 limit 关键字,limit index,pageSize
* index:当前页的起始索引,假设每页 5 条数据
* 比如第 1 页的第一条数据是数据库中的第 1 条数据,所以索引是 0,
* 第 2 页的第一条数据是数据库中的第 6 条数据,索引就是 5
* 第 3 页的第一条数据是数据库中的第 11 条数据,索引就是 10
* pageSize:每页显示的条数
* pageNum:当前页的页码数
* 我们一般都是知道当前页和每页条数
* index 怎么计算呢?比如第 3 页的第一条数据的索引,他是第 3 页
* 他之前就有(3-1)页数据,也就是 (pageNum-1)
* 数量是页数乘以每页条数也就是 (pageNum-1)*pageSize
* 他之前有十条,它就是第十一条数据,也就是 (pageNum-1)*pageSize+1
* 他的索引是序号减一也就是 (pageNum-1)*pageSize+1-1
* 也就是 (pageNum-1)*pageSize
*/
@Test
public void testPageHelper(){
EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class);
}
}
使用分页功能也非常简单,在查询前使用分页插件拦截器即可
@Test
public void testPageHelper(){
EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class);
//每页是 3 条数据,查询第 2 页的
PageHelper.startPage(2,3);
List<Emp> list = mapper.selectByExample(null);
list.forEach(System.out::println);
}
查询后还能获取分页的相关信息
@Test
public void testPageHelper(){
EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class);
PageHelper.startPage(2,3);
List<Emp> list = mapper.selectByExample(null);
PageInfo<Emp> page = new PageInfo<>(list,5);
System.out.println(page);
list.forEach(System.out::println);
}
new PageInfo<>(list,5)
中的第一个参数就是分页后的数据,第二个参数则是导航分页页码数,比如 5,我们知道前端界面的分页功能中会有上一页,下一页,以及当前页所在的导航页比如你在第三页:1 2 3 4 5,如果传的是 3,比如你在第六页:5 6 7
PageInfo 有许多分页信息:
PageInfo{
pageNum=2, pageSize=3, size=3, startRow=4, endRow=6, total=7, pages=3, list=Page{count=true, pageNum=2, pageSize=3, startRow=3, endRow=6, total=7, pages=3, reasonable=false, pageSizeZero=false}, prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]}
常用信息:
pageNum:当前页的页码
pageSize:每页显示的条数
size:当前页显示的真实条数,比如最后一页可能只有一条
total:总记录数
pages:总页数
prePage:上一页的页码
nextPage:下一页的页码
isFirstPage/isLastPage:是否为第一页/最后一页
hasPreviousPage/hasNextPage:是否存在上一页/下一页
navigatePages:导航分页的页码数,你传的第二个参数
navigatepageNums:导航分页的页码,[1,2,3]
https://gitee.com/sky759/my-batis.git