Mybatis框架详解
Mybatis框架(1)---Mybatis入门
mybatis入门
MyBatis是什么?
MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis,实质上Mybatis对ibatis进行一些改进。 目前mybatis在github上托管。 git(分布式版本控制,当前比较流程)
MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。
Mybatis通过xml或注解的方式将要执行的各种statement(statement、preparedStatemnt、CallableStatement)配置起来,并通过java对象和statement中的sql进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射成java对象并返回。
mybatis架构
搭建开发环境
(1)导包
(2)导入配置文件
这里我在工程文件下新建了一个和src平级的文件,把有关mybatis配置文件和src文件分离,看去界面更加清晰,因为在ssh开发中你肯定还要配置其它配置文件
这里的log4j.properties主要是为了在后台输出是更加看的清楚执行流程,这个可要可不要.
(3)配需相关文件属性
User.xml
<?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"> <!-- namespace命名空间,为了对sql语句进行隔离,方便管理 ,mapper开发dao方式,使用namespace有特殊作用 --> <mapper namespace="test"> <!-- 在mapper.xml文件中配置很多的sql语句,执行每个sql语句时,封装为MappedStatement对象 mapper.xml以statement为单位管理sql语句 --> <!-- 根据id查询用户信息 --> <!-- id:唯一标识 一个statement #{}:表示 一个占位符,如果#{}中传入简单类型的参数,#{}中的名称随意 parameterType:输入 参数的类型,通过#{}接收parameterType输入 的参数 resultType:输出结果 类型,不管返回是多条还是单条,指定单条记录映射的pojo类型 --> <select id="findUserById" parameterType="int" resultType="com.study.model.User"> SELECT * FROM USER WHERE id= #{id} </select> <!-- 根据用户名称查询用户信息,可能返回多条 ${}:表示sql的拼接,通过${}接收参数,将参数的内容不加任何修饰拼接在sql中。 --> <select id="findUserByName" parameterType="java.lang.String" resultType="com.study.model.User"> select * from user where username like '%${value}%' </select> <!-- 添加用户 parameterType:输入 参数的类型,User对象 包括 username,birthday,sex,address #{}接收pojo数据,可以使用OGNL解析出pojo的属性值 #{username}表示从parameterType中获取pojo的属性值 selectKey:用于进行主键返回,定义了获取主键值的sql order:设置selectKey中sql执行的顺序,相对于insert语句来说 keyProperty:将主键值设置到哪个属性 resultType:select LAST_INSERT_ID()的结果 类型 --> <insert id="insertUser" parameterType="com.study.model.User"> <selectKey keyProperty="id" order="AFTER" resultType="int"> select LAST_INSERT_ID() </selectKey> INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address}) </insert> <!-- 用户删除 --> <delete id="deleteUser" parameterType="int"> delete from user where id=#{id} </delete> <!-- 用户更新 要求:传入的user对象中包括 id属性值 --> <update id="updateUser" parameterType="com.study.model.User"> update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id=#{id} </update> </mapper>
SqlMapConfig.xml
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 和spring整合后 environments配置将废除--> <environments default="development"> <environment id="development"> <!-- 使用jdbc事务管理--> <transactionManager type="JDBC" /> <!-- 数据库连接池--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8" /> <property name="username" value="root" /> <property name="password" value="root" /> </dataSource> </environment> </environments> <!-- 加载mapper.xml --> <mappers> <mapper resource="sqlmap/User.xml" /> </mappers> </configuration>
User.java
public class User { private int id; private String username;// 用户姓名 private String sex;// 性别 private Date birthday;// 生日 private String address;// 地址 /* *提供set和get方法,和toString方法 * */ }
MybatisFirst 测试类,进行增删改查
import java.io.IOException; import java.io.InputStream; import java.util.Date; import java.util.List; 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 org.junit.Before; import org.junit.Test; import com.guigu.model.User; public class MybatisFirst { // 会话工厂 private SqlSessionFactory sqlSessionFactory; // 创建工厂 @Before //before在text标签之前执行,所以会创建好sqlSessionFactory对象 public void init() throws IOException { // 配置文件(SqlMapConfig.xml) String resource = "SqlMapConfig.xml"; // 加载配置文件到输入 流 InputStream inputStream = Resources.getResourceAsStream(resource); // 创建会话工厂 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } // 根据id查询用户(得到单条记录) @Test public void testFindUserById() { // 通过sqlSessionFactory创建sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 通过sqlSession操作数据库 // 第一个参数:statement的位置,等于namespace+statement的id // 第二个参数:传入的参数 User user = sqlSession.selectOne("test.findUserById", 16); sqlSession.close(); System.out.println(user); } // 模糊查询(可能是单条也可能是多条) @Test public void testFindUserByName() { // 通过sqlSessionFactory创建sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); //selectList代表着返回是list集合 List<User> list = sqlSession.selectList("test.findUserByName", "小明"); sqlSession.close(); System.out.println(list.get(0).getUsername()); } // 添加用户 @Test public void testInsertUser() { // 通过sqlSessionFactory创建sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); User user = new User(); user.setUsername("小小徐"); user.setAddress("杭州市余杭区未来科技城"); user.setBirthday(new Date()); user.setSex("1"); //insert代表插入 sqlSession.insert("test.insertUser", user); //查看是不需要提交事物的,但是插入和修改是需要提交事物的 sqlSession.commit(); sqlSession.close(); //这里输出的id竟然是0,有哪位大神解释下吗? System.out.println("用户的id=" + user.getId()); }
// 根据id删除用户 @Test public void testDeleteUser() { // 通过sqlSessionFactory创建sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // delete代表删除用户 sqlSession.delete("test.deleteUser", 29); sqlSession.commit(); sqlSession.close(); } // 根据id更新用户 @Test public void testUpdateUser() { // 通过sqlSessionFactory创建sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 创建更新数据对象,要求必须包括 id User user = new User(); user.setId(40); user.setUsername("小小钟"); user.setAddress("杭州余杭区东西大道"); //user.setBirthday(new Date()); user.setSex("1"); //update更新数据 sqlSession.update("test.updateUser", user); sqlSession.commit(); sqlSession.close(); System.out.println("用户的id=" + user.getId()); } }
mybatis与hibernate重要区别
企业开发进行技术选型 ,考虑mybatis与hibernate适用场景。
mybatis:入门简单,程序容易上手开发,节省开发成本 。mybatis需要程序员自己编写sql语句,是一个不完全 的ORM框架,对sql修改和优化非常容易实现 。
mybatis适合开发需求变更频繁的系统,比如:互联网项目。
hibernate:入门门槛高,如果用hibernate写出高性能的程序不容易实现。hibernate不用写sql语句,是一个 ORM框架。
hibernate适合需求固定,对象数据模型稳定,中小型项目,比如:企业OA系统。
总之,企业在技术选型时根据项目实际情况,以降低成本和提高系统 可维护性为出发点进行技术选型。
总结
SqlMapConfig.xml
是mybatis全局配置文件,只有一个,名称不固定的,主要mapper.xml,mapper.xml中配置 sql语句
mapper.xml
mapper.xml是以statement为单位进行配置。(把一个sql称为一个statement),satatement中配置 sql语句、parameterType输入参数类型(完成输入映射)、resultType输出结果类型(完成输出映射)。还提供了parameterMap配置输入参数类型(过期了,不推荐使用了),还提供resultMap配置输出结果类型(完成输出映射)
#{}
表示一个占位符,向占位符输入参数,mybatis自动进行java类型和jdbc类型的转换。程序员不需要考虑参数的类型,比如:传入字符串,mybatis最终拼接好的sql就是参数两边加单引号。#{}接收pojo数据,可以使用OGNL解析出pojo的属性值
${}
表示sql的拼接,通过${}接收参数,将参数的内容不加任何修饰拼接在sql中。${}也可以接收pojo数据,可以使用OGNL解析出pojo的属性值
缺点:不能防止sql注入。
selectOne
用于查询单条记录,不能用于查询多条记录,否则异常:
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 4
selectList
用于查询多条记录,可以用于查询单条记录的。
本文就讲到这里,欢迎大家指点,本文引用了博客名叫:凌晨。。。三点,非常感谢!
Mybatis框架(2)---mapper代理方法
mapper代理方法
在我们在写MVC设计的时候,都会写dao层和daoimp实现层,但假如我们使用mapper代理的方法,我们就可以不用先daoimp实现类
当然这得需要遵守一些相应的规则:
(1)Usermapper.java接口必须和Usermapper.xml名称相同,且要在同一目录下:
(2)mapper.xml中namespace等于mapper接口的地址
(3)Usermapper.java接口中国的方法名和Usermapper.xml中statement的id一致
<!-- 7综合查询 --> <select id="findUserCount" parameterType="com.study.model.User" resultType="int"> select count(*) from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%' </select>
如果你在Usermapper.xml配置上面这些属性那么你所写的接口就必须:
1 /*findUserCount接口的名字必须和id属性一致 2 * 传入的参数必须和parameterType是一致,前面是user这里也是user 3 * 返回类型resultType是int类型,那么这里也必须是int类型 4 */ 5 public int findUserCount(User user);
(4)SqlMapConfig.xml中加载mapper.xml
1 <mappers> 2 <!-- 这里是之前加载所写的 --> 3 <!-- <mapper resource="sqlmap/User.xml" /> --> 4 <!-- 通过mapper接口 加载单个映射文件 必须遵循一些规范: 需要将mapper接口和mapper.xml映射文件 文件名必须一致 并且在同一个目录下 --> 5 <mapper class="com.study.mapper.UserMapper" /> 6 7 </mappers>
(5)通过mapper代理方法进行增删改查
a.编写user对象
public class User { private int id; private String username;// 用户姓名 private String sex;// 性别 private Date birthday;// 生日 private String address;// 地址 /* *提供set和get方法和tostring方法 * */ }
b.配置SqlMapConfig.xml
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="db.properties"></properties> <!-- 定义 别名 --> <typeAliases> <!-- 单个别名的定义 alias:别名,type:别名映射的类型 --> <!-- <typeAlias type="com.study.model.User" alias="user"/> --> <!-- 批量别名定义 指定包路径,自动扫描包下边的pojo,定义别名,别名默认为类名(首字母小写或大写) --> <package name="com.study.model"/> </typeAliases> <!-- 和spring整合后 environments配置将废除--> <environments default="development"> <environment id="development"> <!-- 使用jdbc事务管理--> <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> <!--加载mapper映射 如果将和spring整合后,可以使用整合包中提供的mapper扫描器,此处的mappers不用配置了。 --> <mappers> <mapper class="com.study.mapper.UserMapper" /> </mappers> </configuration>
在这里有两个新的知识点:
1: <properties resource="db.properties"></properties>
之前在连接数据库填写配置文件直接把属性(连接数据库用户名,密码等)写在里面,而这里是写在外面的db.properties中,这样更好的体现代码的灵活性
2:<typeAliases>标签,之前我们配置mapper.xml文件中的parameterType和resultType的属性如果是对象一定要写类的全名称,而通过<typeAliases>标签的配置我们只需要写类的名字就好了
c.配置db.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc\:mysql\://localhost\:3306/study jdbc.username=root jdbc.password=root
也就是这样的
d.配置UserMapper.xml
<?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"> <!-- namespace的属性对应所在的UserMapper接口全名称 --> <mapper namespace="com.study.mapper.UserMapper"> <!-- 发现这里的resultType属性我们可以不用写类的全名称com.study.model.User,因为在--> <!--SqlMapConfig.xml属性中我们配置了<typeAliases>标签 --> <!-- 根据id查询用户信息 --> <select id="findUserById" parameterType="int" resultType="user"> SELECT * FROM USER WHERE id= #{id} </select> <!-- 根据用户名称查询用户信息,可能返回多条--> <select id="findUserByName" parameterType="java.lang.String" resultType="user"> select * from user where username like '%${value}%' </select> <!-- 添加用户--> <insert id="insertUser" parameterType="user"> INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address}) </insert> </mapper>
f.配置UserMapper.java对象
public interface UserMapper { //根据用户id查询用户信息 public User findUserById(int id) throws Exception; //根据用户名称 查询用户信息 public List<User> findUserByName(String username) throws Exception;//插入用户 public void insertUser(User user)throws Exception;
//删除用户 public void deleteUser(int id) throws Exception; //修改用户 public void updateUser(User user) throws Exception; }
e.编写UserMapperTest类进行 增删改查
1 import java.io.IOException; 2 import java.io.InputStream; 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import org.apache.ibatis.io.Resources; 7 import org.apache.ibatis.session.SqlSession; 8 import org.apache.ibatis.session.SqlSessionFactory; 9 import org.apache.ibatis.session.SqlSessionFactoryBuilder; 10 import org.junit.Before; 11 import org.junit.Test; 12 13 import com.study.mapper.UserMapper; 14 import com.study.model.User; 15 16 17 public class UserMapperTest { 18 // 会话工厂 19 private SqlSessionFactory sqlSessionFactory; 20 // 创建工厂 21 @Before 22 public void init() throws IOException { 23 String resource = "SqlMapConfig.xml"; 24 InputStream inputStream = Resources.getResourceAsStream(resource); 25 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 26 } 27 //通过用户id查找对象 28 @Test 29 public void testFindUserById() throws Exception { 30 SqlSession sqlSession = sqlSessionFactory.openSession(); 31 // 创建代理对象,这里就相当于有事先类了 32 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 33 User user = userMapper.findUserById(1); 34 System.out.println(user); 35 } 36 37 //根据用户相信模糊查询 38 @Test 39 public void testFindUserByUsername() throws Exception { 40 SqlSession sqlSession = sqlSessionFactory.openSession(); 41 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 42 List<User> list = userMapper.findUserByName("小明"); 43 System.out.println(list); 44 } 45 46 //添加用户 47 @Test 48 public void testInsertUser() throws Exception { 49 SqlSession sqlSession = sqlSessionFactory.openSession(); 50 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 51 User user = new User(); 52 user.setUsername("小小洪"); 53 //我这里只添加了用户名,其它信息没有添加,默认为null 54 //Preparing: INSERT INTO USER(username,birthday,sex,address) VALUES(?,?,?,?) 55 //Parameters: 小小洪(String), null, null, null 56 userMapper.insertUser(user); 57 sqlSession.commit(); 58 sqlSession.close(); 59 } 60 } 61 /* 62 *删除和修改我这里先不写了,大家理解就好 63 */
本文就讲到这,谢谢大家,欢迎大家指点谢谢!
Mybatis框架(3)---SqlMapConfig.xml解析
SqlMapConfig.xml
SqlMapConfig.xml是Mybatis的全局配置参数,关于他的具体用的有专门的MyBatis - API文档,这里面讲的非常清楚,所以我这里就挑几个讲下:
他的主要配置的属性有如下:
1.properties 属性
这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递。
1 <!-- 加载属性文件 --> 2 <properties resource="db.properties"> 3 <!-- 可以在配置相关的其他事项 --> 4 <!-- <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/> --> 5 </properties> 6 <!-- 这里如果在db.properties和 name=""都配置了jdbc.driver那么优先执行name中的--> 7 <!-- 配置全局的参数信息 -->
如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:
- 在 properties 元素体内指定的属性首先被读取。
- 然后根据 properties 元素中的 resource 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。
- 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。
因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的是 properties 属性中指定的属性。
2.settings全局的参数配置
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。
具体的就不写了只写一个表达式:
<!-- 配置全局的参数信息 --> <settings> <setting name="" value=""/> </settings>
3.typeAliases(别名)
制定别名最大的一个优势就是方便我们的开发,因为我们如果没有设置别名的情况下,在mapper.xml中
定义了很多Statement ,Statement需要parameterType指定输入参数的类型或者指定输出结果的类型比如:
<!-- 根据id查询用户信息 --> <select id="findUserById" parameterType="int" resultType="com.study.model.User"> SELECT * FROM USER WHERE id= #{id} </select>
这里的resultType如果是对象一定要类的全名称,那我们能不能只写了user就能达到同样的效果,这样是不是就有利于简便我们的开发
有两种方法:
1.单个别名的定义
1 <typeAliases> 2 <!-- 3 单个别名的定义 4 alias:别名,type:别名映射的类型 --> 5 <!-- <typeAlias type="com.study.model.User" alias="user"/> --> 6 </typeAliases> 7 <!--这样的话只需输入user就能起到一样效果-->
2.批量定义别名(常用)
1 <typeAliases> 2 <!-- 批量别名定义 3 指定包路径,自动扫描包下边的pojo,定义别名,别名默认为类名(首字母小写或大写) 4 --> 5 <package name="com.study.model"/> 6 </typeAliases>
4.mappers(映射配置)
1.通过resource加载单个映射文件
1 <!-- 加载映射文件 --> 2 <mappers> 3 <mapper resource="sqlmap/User.xml" /> 4 </mappers>
2.通过mapper接口加载单个mapper
1 <mappers> 2 <!-- 通过mapper接口 加载单个映射文件 必须遵循一些规范: 需要将mapper接口和mapper.xml映射文件 文件名必须一致 并且在同一个目录下 --> 3 <mapper class="com.study.mapper.UserMapper" /> 4 </mappers>
3.批量加载mapper(推荐使用)
1 <mappers> 2 <!-- 3 指定mapper接口的包名 mybatis会自动扫描这个包下所有的mapper接口 然后执行加载 4 --> 5 <package name="com.study.mapper"/> 6 </mappers>
其他的我就不讲了,要详细的可以找api,非常的详细,欢迎大家指点,谢谢!
Mybatis框架(4)---输入输出映射
输入输出映射
通过parameterType制定输入参数类型 类型可以是简单类型(int String)也可以是POJO本身 或者包装类
1输入映射
关于输入简单类型和pojo本身的我就不写了,因为比较简单,下面我主要举一个包装类的例子:
使用包装类POJO 将复杂的查询条件封装到POJO中
1 //当你继承user属性后,你就可以在user的基础上添加自己的属性了 2 public class UserCustomer extends User { 3 4 //用户的基本信息 5 //可以扩展用户的信息 6 //其他信息 7 }
查询条件封装的类
1 public class UserQueryVo { 2 3 //这里包装需要查询的条件 4 5 private UserCustomer userCustomer; 6 7 public UserCustomer getUserCustomer() { 8 return userCustomer; 9 } 10 11 public void setUserCustomer(UserCustomer userCustomer) { 12 this.userCustomer = userCustomer; 13 }
UserMapper.xml
1 <!-- 2 #{userCustomer.sex} 取出pojo对象 中性别的属性值 3 ${userCustomer.username}取出pojo中 用户的名称 4 --> 5 <select id="findUserList" parameterType="com.study.model.UserQueryVo" resultType="com.study.model.UserCustomer"> 6 select * from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%' 7 </select
UserMapper.java
//用户综合信息查询 public List<UserCustomer> findUserList(UserQueryVo userQueryVo) throws Exception;
测试代码
1 @Test 2 public void testFindUserList() throws Exception{ 3 SqlSession sqlSession =sqlSessionFactory.openSession(); 4 //创建UserMapper 对象 MyBatis自动生成代理对象 5 UserMapper userMapper =sqlSession.getMapper(UserMapper.class); 6 7 //创建包装对象 设置查询条件 8 UserQueryVo userQueryVo =new UserQueryVo(); 9 10 UserCustomer userCustomer =new UserCustomer(); 11 userCustomer.setSex("1"); 12 userCustomer.setUsername("小明"); 13 userQueryVo.setUserCustomer(userCustomer); 14 15 //完成查询 16 List<UserCustomer> list =userMapper.findUserList(userQueryVo); 17 System.out.println(list); 18 }
2.输出映射
(1)resultType
使用resultType进行输出映射的时候 只有查询出来的列名和pojo 对应的属性名完全一致 才可以映射
如果查询出来的列名和pojo中的属性完全不一致 没有创建pojo对象
如果查询出来的列名和pojo中的属性只有部分一致 ,就会创建pojo对象 ,不一致的属性值为null
举例:查询用户总人数
mapper.xml
1 <!-- 7综合查询 --> 2 <select id="findUserCount" parameterType="com.guigu.model.UserQueryVo" resultType="int"> 3 select count(*) from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%' 4 </select>
mapper.java
public int findUserCount(UserQueryVo userQueryVo);
测试代码
@Test public void testFindUserCount() throws Exception{ SqlSession sqlSession =sqlSessionFactory.openSession(); //创建UserMapper 对象 MyBatis自动生成代理对象 UserMapper userMapper =sqlSession.getMapper(UserMapper.class); //创建包装对象 设置查询条件 UserQueryVo userQueryVo =new UserQueryVo(); UserCustomer userCustomer =new UserCustomer(); userCustomer.setSex("1"); userCustomer.setUsername("小明"); userQueryVo.setUserCustomer(userCustomer); int count =userMapper.findUserCount(userQueryVo); System.out.println(count); }
在输出参数中,不论你返回的是单个对象还是对象的集合,在resulttype中都只需要写该对象的全名称就可以了
(2)resultMap
resultMap到底做什么用的呢?下面我来举个例子:
比如有下面的mapper.xml配置
<!-- mapper执行语句 --> <!-- #{userCustomer.sex} 取出pojo对象 中性别的属性值 ${userCustomer.username}取出pojo中 用户的名称 --> <select id="findUserList" parameterType="com.guigu.model.UserQueryVo" resultType="com.guigu.model.UserCustomer"> select id id_,username username_,birthday birthday_,address from user where user.sex=#{userCustomer.sex} and user.username like '%${userCustomer.username}%' </select>
那么运行的输出结果:会发现只有地址能够完成赋值,而其它因为采用别名无法赋值:
得出结论:
如果查询出来的列名和pojo中的属性完全不一致 没有创建pojo对象
如果查询出来的列名和pojo中的属性只有部分一致 ,就会创建pojo对象 ,不一致的属性值为null
上面的问题那如何解决,其实也很简单就是配置resultMap:
<!-- type resultMap最终映射的java对象类型 可以使用别名,因为本来是要写类的全名称,这里输入的就是别名 id 对resultMap唯一的标识符,这里的id要和下面的resultMap中的内容一致 --> <resultMap type="user" id="userResultMap"> <!-- id表示查询结果集中唯一的标识 主键 column 查询出来的列名 property type pojo中对应的属性名 --> <id column="id_" property="id"/> <!-- result对普通名的映射 column 查询出来的列名 property type pojo中对应的属性名 --> <result column="username_" property="username"/> <result column="birthday_" property="birthday"/> </resultMap> <!-- 配置结果集类型 --> <select id="findUserByIdResultMap" parameterType="int" resultMap="userResultMap"> select id id_,username username_,birthday birthday_,address from user where id=#{value} </select>
总结:
使用resultType 进行输出映射 只有查询出来的列名 和pojo中的属性名一致的时候才可以映射成功 。
如果查询出来的列名和pojo属性名不一致 可以通过定义一个resultMap对列名和pojo属性之间做一个映射。
本文就讲到这里,欢迎大家多多指点,哪里需要修正或者补充,欢迎留言,谢谢!
Mybatis框架(5)---动态sql
那么,问题来了: 什么是动态SQL? 动态SQL有什么作用?
传统的使用JDBC的方法,相信大家在组合复杂的的SQL语句的时候,需要去拼接,稍不注意哪怕少了个空格,都会导致错误。Mybatis的动态SQL功能正是为了解决这种问题, 其通过 if, choose, when, otherwise, trim, where, set, foreach标签,可组合成非常灵活的SQL语句,从而提高开发人员的效率。下面就去感受Mybatis动态SQL的魅力吧:
1. if: 你们能判断,我也能判断!
作为程序猿,谁不懂 if ! 在mybatis中也能用 if 啦:
<select id="findUserById" resultType="user"> select * from user where <if test="id != null"> id=#{id} </if> and deleteFlag=0; </select>
上面例子: 如果传入的id 不为空, 那么才会SQL才拼接id = #{id}。 这个相信大家看一样就能明白,不多说。
细心的人会发现一个问题:“你这不对啊! 要是你传入的id为null, 那么你这最终的SQL语句不就成了 select * from user where and deleteFlag=0, 这语句有问题!”
是啊,这时候,mybatis的 where 标签就该隆重登场啦:
2. where, 有了我,SQL语句拼接条件神马的都是浮云!
咱们通过where改造一下上面的例子:
<select id="findUserById" resultType="user"> select * from user <where> <if test="id != null"> id=#{id} </if> and deleteFlag=0; </where> </select>
有些人就要问了: “你这都是些什么玩意儿! 跟上面的相比, 不就是多了个where标签嘛! 那这个还会不会出现 select * from user where and deleteFlag=0 ?”
的确,从表面上来看,就是多了个where标签而已, 不过实质上, mybatis是对它做了处理,当它遇到AND或者OR这些,它知道怎么处理。其实我们可以通过 trim 标签去自定义这种处理规则。
3. trim : 我的地盘,我做主!
上面的where标签,其实用trim 可以表示如下:
<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>
它的意思就是: 当WHERE后紧随AND或则OR的时候,就去除AND或者OR。 除了WHERE以外, 其实还有一个比较经典的实现,那就是SET。
4. set: 信我,不出错!
<update id="updateUser" parameterType="com.dy.entity.User"> update user set <if test="name != null"> name = #{name}, </if> <if test="password != null"> password = #{password}, </if> <if test="age != null"> age = #{age} </if> <where> <if test="id != null"> id = #{id} </if> and deleteFlag = 0; </where> </update>
问题又来了: “如果我只有name不为null, 那么这SQL不就成了 update set name = #{name}, where ........ ? 你那name后面那逗号会导致出错啊!”
是的,这时候,就可以用mybatis为我们提供的set 标签了。下面是通过set标签改造后:
<update id="updateUser" parameterType="com.dy.entity.User"> update user <set> <if test="name != null">name = #{name},</if> <if test="password != null">password = #{password},</if> <if test="age != null">age = #{age},</if> </set> <where> <if test="id != null"> id = #{id} </if> and deleteFlag = 0; </where> </update>
这个用trim 可表示为:
<trim prefix="SET" suffixOverrides=","> ... </trim>
WHERE是使用的 prefixOverrides(前缀), SET是使用的 suffixOverrides (后缀), 看明白了吧!
5. foreach: 你有for, 我有foreach, 不要以为就你才屌!
java中有for, 可通过for循环, 同样, mybatis中有foreach, 可通过它实现循环,循环的对象当然主要是java容器和数组。
<select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </select>
将一个 List 实例或者数组作为参数对象传给 MyBatis,当这么做的时候,MyBatis 会自动将它包装在一个 Map 中并以名称为键。List 实例将会以“list”作为键,而数组实例的键将是“array”。同样, 当循环的对象为map的时候,index其实就是map的key。
6. choose: 我选择了你,你选择了我!
Java中有switch, mybatis有choose。
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != null"> AND title like #{title} </when> <when test="author != null and author.name != null"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose> </select>
以上例子中: 当title和author都不为null的时候, 那么选择二选一(前者优先), 如果都为null, 那么就选择 otherwise中的, 如果tilte和author只有一个不为null, 那么就选择不为null的那个。
纵观mybatis的动态SQL, 强大而简单, 相信大家简单看一下就能使用了。
好啦,本次就写到这!下篇文章将结合mybatis的源码分析一次sql语句执行的整个过程。
Mybatis框架(6)---Mybatis插入数据后获取自增主键
Mybatis插入数据后获取自增主键
首先理解这就话的意思:就是在往数据库表中插入一条数据的同时,返回该条数据在数据库表中的自增主键值。
有什么用呢,举个例子:
你编辑一条新闻,同时需要给该新闻打上标签(可以一个或者多个:比如:钱等等),然后存储到数据库中。怎么存,肯定涉及到三张表,新闻表,标签表,新闻标签id关联表
新闻表插入数据简单,标签表插入数据简单。那新闻标签表呢,如何关联,那是不是需要新闻表和标签表插入数据的时候,返回它们的主键Id然后再存储到新闻标签表中。
这种场景还是蛮常见的。下面主要针对的MySQL数据库进行操作。
1.TLivePressOriginDOMapper.xml插入语句添加配置
<!-- 主要讲新添加的两个属性:useGeneratedKeys和keyProperty--> <!--useGeneratedKeys="true" 默认值是:false。 含义:设置是否使用JDBC的getGenereatedKeys方法获取主键并赋值到keyProperty设置的领域模型属性中。--> <!--keyProperty="autoId" 就很好理解了,就是把主键值赋值给TLivePressOriginDO实体的autoId属性中--> <insert id="insertSelective" parameterType="com.jincou.dlo.TLivePressOriginDO" useGeneratedKeys="true" keyProperty="autoId">
2.查看TLivePressOriginDO实体
有个属性autoId
3.在看TLivePressOriginDOMapper
4、在看实际效果
我们看到这里数据的主键值是10,是通过赋值给bean实体中的autoId属性的。
那到底数据库存储该条数据的主键是不是10呢?
5、看数据库该条记录
数据库中该条数据的自增主键Id果然是10,那就说明达到了插入数据的同时获得了该条数据在数据库表中的主键值的目的。
注意:以上操作只针对MySQL数据库哦。
Mybatis框架(7)---Mybatis逆向工程
逆向工程的目的就是缩减了我们的开发时间。所谓Mybatis逆向工程,就是Mybatis会根据我们设计好的数据表,自动生成pojo
、mapper
以及mapper.xml
。
接下来就是项目搭建过程。github源码
:mybatis逆向工程代码
一、pom.xml文件
<!--连接mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<!--mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<!-- mybatis 逆向生成工具 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
二、generatorConfig.xml
这是配置逆向工程配置信息。
<?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>
<!--一个context对应一个数据库-->
<context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!--设置父类mapper,这样一来所有逆向工程生成的mapper都会继承该mapper-->
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="com.binron.mapper.base.BaseMapper"/>
</plugin>
<!--连接数据库信息-->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://47.99.888.55:3306/binron"
userId="root"
password="root">
</jdbcConnection>
<!-- 对于生成的pojo所在包 -->
<javaModelGenerator targetPackage="com.binron.pojo" targetProject="src/main/java"/>
<!-- 对于生成的mapper所在目录 -->
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/>
<!-- 配置mapper对应的java映射 -->
<javaClientGenerator targetPackage="com.binron.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<!--需要逆向工程的表-->
<table tableName="users"></table>
<table tableName="my_friends"></table>
<table tableName="friends_request"></table>
<table tableName="chat_msg"></table>
</context>
</generatorConfiguration>
三、父类BaseMapper
在配置信息中,父类mapper不是必须的,不过一般企业开发中,肯定是有父类的,因为你不可能每个mapper都写增删改查方法,完全可以抽离。
/**
* @author 基础的Mapper 所有业务表都继承之该mapper
*/
public interface BaseMapper<T> extends Mapper<T>, MySqlMapper<T> {
//FIXME 特别注意,该接口不能被扫描到,否则会出错
/**
* 通过主键删除
*/
int deleteByPrimaryKey(String id);
/**
* 插入对象
*/
int insert(T record);
/**
* 通过K 查找对象
*/
T selectByPrimaryKey(String id);
/**
* 查找所有
*/
List<T> selectAll();
/**
* 更新 对象
*/
int updateByPrimaryKey(T record);
}
四、GeneratorDisplay(启动类)
public class GeneratorDisplay {
public void generator() throws Exception{
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
//指定 逆向工程配置文件
File configFile = new File("generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
callback, warnings);
myBatisGenerator.generate(null);
}
public static void main(String[] args) throws Exception {
try {
GeneratorDisplay generatorSqlmap = new GeneratorDisplay();
generatorSqlmap.generator();
} catch (Exception e) {
e.printStackTrace();
}
}
}
总体就是这么的简单。
注意点
1、对于一张表已经完成逆向工程后,不能再执行一次main方法,除非在generatorConfig.xml删除这个table,因为如果依次执行两次main,那么mapper.xml
内容会叠加。
2、如果你的表有变动,那么你可以选择先删除该表所以创建好的逆向工程,重新生成,要么手动修改内容。
Mybatis框架(8)---Mybatis插件原理(代理+责任链)
在实际开发过程中,我们经常使用的Mybaits插件就是分页插件了,通过分页插件我们可以在不用写count语句和limit的情况下就可以获取分页后的数据,给我们开发带来很大
的便利。除了分页,插件使用场景主要还有更新数据库的通用字段,分库分表,加解密等的处理。
这篇博客主要讲Mybatis插件原理,下一篇博客会设计一个Mybatis插件实现的功能就是每当新增数据的时候不用数据库自增ID而是通过该插件生成雪花ID,作为每条数据的主键。
一、JDK动态代理+责任链设计模式
Mybatis的插件其实就是个拦截器功能。它利用JDK动态代理和责任链设计模式的综合运用
。采用责任链模式,通过动态代理组织多个拦截器,通过这些拦截器你可以做一些
你想做的事。所以在讲Mybatis拦截器之前我们先说说JDK动态代理+责任链设计模式。有关JDK动态代理的原理,可以参考我之前写的一篇博客:【java设计模式】---代理模式
1、JDK动态代理案例
public class MyProxy {
/**
* 一个接口
*/
public interface HelloService{
void sayHello();
}
/**
* 目标类实现接口
*/
static class HelloServiceImpl implements HelloService{
@Override
public void sayHello() {
System.out.println("sayHello......");
}
}
/**
* 自定义代理类需要实现InvocationHandler接口
*/
static class HWInvocationHandler implements InvocationHandler {
/**
* 目标对象
*/
private Object target;
public HWInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------插入前置通知代码-------------");
//执行相应的目标方法
Object rs = method.invoke(target,args);
System.out.println("------插入后置处理代码-------------");
return rs;
}
public static Object wrap(Object target) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),new HWInvocationHandler(target));
}
}
public static void main(String[] args) {
HelloService proxyService = (HelloService) HWInvocationHandler.wrap(new HelloServiceImpl());
proxyService.sayHello();
}
}
运行结果
------插入前置通知代码-------------
sayHello......
------插入后置处理代码-------------
2、优化
上面代理的功能是实现了,但是有个很明显的缺陷,就是HWInvocationHandler是动态代理类
,也可以理解成是个工具类,我们不可能会把业务代码写到写到到invoke方法
里,
不符合面向对象的思想,可以抽象一下处理。可以设计一个Interceptor接口
,需要做什么拦截处理实现接口就行了。
public interface Interceptor {
/**
* 具体拦截处理
*/
void intercept();
}
intercept() 方法就可以处理各种前期准备了
public class LogInterceptor implements Interceptor {
@Override
public void intercept() {
System.out.println("------插入前置通知代码-------------");
}
}
public class TransactionInterceptor implements Interceptor {
@Override
public void intercept() {
System.out.println("------插入后置处理代码-------------");
}
}
代理对象也做一下修改
public class HWInvocationHandler implements InvocationHandler {
private Object target;
private List<Interceptor> interceptorList = new ArrayList<>();
public TargetProxy(Object target,List<Interceptor> interceptorList) {
this.target = target;
this.interceptorList = interceptorList;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//处理多个拦截器
for (Interceptor interceptor : interceptorList) {
interceptor.intercept();
}
return method.invoke(target, args);
}
public static Object wrap(Object target,List<Interceptor> interceptorList) {
HWInvocationHandler targetProxy = new HWInvocationHandler(target, interceptorList);
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),targetProxy);
}
}
现在可以根据需要动态的添加拦截器了,在每次执行业务代码sayHello()之前都会拦截,看起来高级一点,来测试一下
public class Test {
public static void main(String[] args) {
List<Interceptor> interceptorList = new ArrayList<>();
interceptorList.add(new LogInterceptor());
interceptorList.add(new TransactionInterceptor());
HelloService target = new HelloServiceImpl();
Target targetProxy = (Target) TargetProxy.wrap(target,interceptorList);
targetProxy.sayHello();
}
}
运行结果
------插入前置通知代码-------------
------插入后置处理代码-------------
sayHello......
3、再优化
上面的动态代理确实可以把代理类中的业务逻辑抽离出来,但是我们注意到,只有前置代理,无法做到前后代理,所以还需要在优化下。所以需要做更一步的抽象,
把拦截对象信息进行封装,作为拦截器拦截方法的参数,把拦截目标对象真正的执行方法放到Interceptor中完成,这样就可以实现前后拦截,并且还能对拦截
对象的参数等做修改。设计一个Invocation 对象
。
public class Invocation {
/**
* 目标对象
*/
private Object target;
/**
* 执行的方法
*/
private Method method;
/**
* 方法的参数
*/
private Object[] args;
//省略getset
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
/**
* 执行目标对象的方法
*/
public Object process() throws Exception{
return method.invoke(target,args);
}
}
Interceptor拦截接口做修改
public interface Interceptor {
/**
* 具体拦截处理
*/
Object intercept(Invocation invocation) throws Exception;
}
Interceptor实现类
public class TransactionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception{
System.out.println("------插入前置通知代码-------------");
Object result = invocation.process();
System.out.println("------插入后置处理代码-------------");
return result;
}
}
Invocation 类就是被代理对象的封装,也就是要拦截的真正对象。HWInvocationHandler修改如下:
public class HWInvocationHandler implements InvocationHandler {
private Object target;
private Interceptor interceptor;
public TargetProxy(Object target,Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(target,method,args);
return interceptor.intercept(invocation);
}
public static Object wrap(Object target,Interceptor interceptor) {
HWInvocationHandler targetProxy = new HWInvocationHandler(target, interceptor);
return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),targetProxy);
}
}
测试类
public class Test {
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
Interceptor transactionInterceptor = new TransactionInterceptor();
HelloService targetProxy = (Target) TargetProxy.wrap(target,transactionInterceptor);
targetProxy.sayHello();
}
}
运行结果
------插入前置通知代码-------------
sayHello......
------插入后置处理代码-------------
4、再再优化
上面这样就能实现前后拦截,并且拦截器能获取拦截对象信息。但是测试代码的这样调用看着很别扭,对应目标类来说,只需要了解对他插入了什么拦截就好。
再修改一下,在拦截器增加一个插入目标类的方法。
public interface Interceptor {
/**
* 具体拦截处理
*/
Object intercept(Invocation invocation) throws Exception;
/**
* 插入目标类
*/
Object plugin(Object target);
}
public class TransactionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception{
System.out.println("------插入前置通知代码-------------");
Object result = invocation.process();
System.out.println("------插入后置处理代码-------------");
return result;
}
@Override
public Object plugin(Object target) {
return TargetProxy.wrap(target,this);
}
}
这样目标类仅仅需要在执行前,插入需要的拦截器就好了,测试代码:
public class Test {
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
Interceptor transactionInterceptor = new TransactionInterceptor();
//把事务拦截器插入到目标类中
target = (HelloService) transactionInterceptor.plugin(target);
target.sayHello();
}
}
运行结果
------插入前置通知代码-------------
sayHello......
------插入后置处理代码-------------
5、多个拦截器如何处理
到这里就差不多完成了,那我们再来思考如果要添加多个拦截器呢,怎么搞?
public class Test {
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
Interceptor transactionInterceptor = new TransactionInterceptor();
target = (HelloService) transactionInterceptor.plugin(target);
LogInterceptor logInterceptor = new LogInterceptor();
target = (HelloService)logInterceptor.plugin(target);
target.sayHello();
}
}
运行结果
------插入前置通知代码-------------
------插入前置通知代码-------------
sayHello......
------插入后置处理代码-------------
------插入后置处理代码-------------
6、责任链设计模式
其实上面已经实现的没问题了,只是还差那么一点点,添加多个拦截器的时候不太美观,让我们再次利用面向对象思想封装一下。我们设计一个InterceptorChain 拦截器链类
public class InterceptorChain {
private List<Interceptor> interceptorList = new ArrayList<>();
/**
* 插入所有拦截器
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptorList) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptorList.add(interceptor);
}
/**
* 返回一个不可修改集合,只能通过addInterceptor方法添加
* 这样控制权就在自己手里
*/
public List<Interceptor> getInterceptorList() {
return Collections.unmodifiableList(interceptorList);
}
}
其实就是通过pluginAll() 方法包一层把所有的拦截器插入到目标类去而已。测试代码:
public class Test {
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
Interceptor transactionInterceptor = new TransactionInterceptor();
LogInterceptor logInterceptor = new LogInterceptor();
InterceptorChain interceptorChain = new InterceptorChain();
interceptorChain.addInterceptor(transactionInterceptor);
interceptorChain.addInterceptor(logInterceptor);
target = (Target) interceptorChain.pluginAll(target);
target.sayHello();
}
}
这里展示的是JDK动态代理+责任链设计模式
,那么Mybatis拦截器就是基于该组合进行开发。
二、Mybatis Plugin 插件概念
1、原理
Mybatis的拦截器实现机制跟上面最后优化后的代码非常的相似。它也有个代理类Plugin
(就是上面的HWInvocationHandler)这个类同样也会实现了InvocationHandler接口
,
当我们调用ParameterHandler,ResultSetHandler,StatementHandler,Executor的对象的时候,,就会执行Plugin的invoke方法
,Plugin在invoke方法中根据
@Intercepts
的配置信息(方法名,参数等)动态判断是否需要拦截该方法.再然后使用需要拦截的方法Method封装成Invocation,并调用Interceptor的proceed
方法。
这样我们就达到了拦截目标方法的结果。例如Executor的执行大概是这样的流程:
拦截器代理类对象->拦截器->目标方法
Executor.Method->Plugin.invoke->Interceptor.intercept->Invocation.proceed->method.invoke。
2、如何自定义拦截器?
1) Interceptor接口
首先Mybatis官方早就想到我们开发会有这样的需求,所以开放了一个org.apacheibatis.plugin.Interceptor
这样一个接口。这个接口就是和上面Interceptor性质是一样的
public interface Interceptor {
//当plugin函数返回代理,就可以对其中的方法进行拦截来调用intercept方法
Object intercept(Invocation invocation) throws Throwable;
//plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。
Object plugin(Object target);
//在Mybatis配置文件中指定一些属性
void setProperties(Properties properties);
}
2)自定义拦截器
这里的ExamplePlugin
和上面的LogInterceptor和TransactionInterceptor
性质是一样的
@Intercepts({@Signature( type= Executor.class, method = "update", args ={MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
}
3)、全局xml配置
最后如果你使用的是Mybatis.xml
也就是Mybatis本身单独的配置,你可以需要在这里配置相应的拦截器名字等。
如果你使用的是spring管理的Mybatis,那么你需要在Spring配置文件
里面配置注册相应的拦截器。
这样一个自定义mybatis插件流程大致就是这样了。
3、Mybatis四大接口
竟然Mybatis是对四大接口进行拦截的,那我们要先要知道Mybatis的四大接口对象 Executor
, StatementHandle
, ResultSetHandler
, ParameterHandler
。
1.Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) MyBatis的执行器,用于执行增删改查操作;
2.ParameterHandler (getParameterObject, setParameters) 处理SQL的参数对象;
3.ResultSetHandler (handleResultSets, handleOutputParameters) 处理SQL的返回结果集;
4.StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理
上图Mybatis框架的整个执行过程。
三、Mybatis Plugin 插件源码
经过上面的分析,再去看Mybastis Plugin 源码的时候就很轻松了。
这几个也就对应上面的几个,只不过添加了注解,来判断是否拦截指定方法。
1、拦截器链InterceptorChain
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
//循环调用每个Interceptor.plugin方法
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
这个就和我们上面实现的是一样的。定义了拦截器链
2、Configuration
通过初始化配置文件把所有的拦截器添加到拦截器链中。
public class Configuration {
protected final InterceptorChain interceptorChain = new InterceptorChain();
//创建参数处理器
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
//创建ParameterHandler
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//插件在这里插入
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
//创建结果集处理器
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
//创建DefaultResultSetHandler
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
//插件在这里插入
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
//创建语句处理器
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//创建路由选择语句处理器
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//插件在这里插入
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
//产生执行器
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
//这句再做一下保护,囧,防止粗心大意的人将defaultExecutorType设成null?
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//然后就是简单的3个分支,产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//此处调用插件,通过插件可以改变Executor行为
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
从代码可以看出Mybatis 在实例化Executor、ParameterHandler、ResultSetHandler、StatementHandler四大接口对象的时候调用interceptorChain.pluginAll()
方法插入
进去的。其实就是循环执行拦截器链所有的拦截器的plugin() 方法,mybatis官方推荐的plugin方法是Plugin.wrap() 方法,这个类就是我们上面的TargetProxy类。
3、Plugin
这里的Plugin就是我们上面的自定义代理类TargetProxy类
public class Plugin implements InvocationHandler {
public static Object wrap(Object target, Interceptor interceptor) {
//从拦截器的注解中获取拦截的类名和方法信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor)
Class<?> type = target.getClass();
//取得接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//产生代理,是Interceptor注解的接口的实现类才会产生代理
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取需要拦截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//是Interceptor实现类注解的方法才会拦截处理
if (methods != null && methods.contains(method)) {
//调用Interceptor.intercept,也即插入了我们自己的逻辑
return interceptor.intercept(new Invocation(target, method, args));
}
//最后还是执行原来逻辑
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
//取得签名Map,就是获取Interceptor实现类上面的注解,要拦截的是那个类(Executor,ParameterHandler, ResultSetHandler,StatementHandler)的那个方法
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//取Intercepts注解,例子可参见ExamplePlugin.java
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
//必须得有Intercepts注解,没有报错
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//value是数组型,Signature的数组
Signature[] sigs = interceptsAnnotation.value();
//每个class里有多个Method需要被拦截,所以这么定义
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
//取得接口
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
//拦截其他的无效
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
4、Interceptor接口
public interface Interceptor {
//拦截
Object intercept(Invocation invocation) throws Throwable;
//插入
Object plugin(Object target);
//设置属性(扩展)
void setProperties(Properties properties);
}
思路
这么下来思路就很清晰了,我们通过实现Interceptor类实现自定义拦截器,然后把它放入InterceptorChain(拦截器链)中,然后通过JDK动态代理来实现依次拦截处理。
Mybatis框架(9)---Mybatis自定义插件生成雪花ID做为表主键项目
先附上项目项目GitHub地址
spring-boot-mybatis-interceptor
有关Mybatis雪花ID主键插件前面写了两篇博客作为该项目落地的铺垫。
该插件项目可以直接运用于实际开发中,作为分布式数据库表主键ID使用。
一、项目概述
1、项目背景
在生成表主键ID时,我们可以考虑主键自增 或者 UUID,但它们都有很明显的缺点
主键自增
:1、自增ID容易被爬虫遍历数据。2、分表分库会有ID冲突。
UUID
: 1、太长,并且有索引碎片,索引多占用空间的问题 2、无序。
雪花算法就很适合在分布式场景下生成唯一ID,它既可以保证唯一又可以排序,该插件项目的原理是
通过拦截器拦截Mybatis的insert语句,通过自定义注解获取到主键,并为该主键赋值雪花ID,插入数据库中。
2、技术架构
项目总体技术选型
SpringBoot2.1.7 + Mybatis + Maven3.5.4 + Mysql + lombok(插件)
3、使用方式
在你需要做为主键的属性上添加@AutoId
注解,那么通过插件可以自动为该属性赋值主键ID。
public class TabUser {
/**
* id(添加自定义注解)
*/
@AutoId
private Long id;
/**
* 姓名
*/
private String name;
//其它属性 包括get,set方法
}
4、项目测试
配置好数据库连接信息,直接启动Springboot启动类Application.java
,访问localhost:8080/save-foreach-user
就可以看到数据库数据已经有雪花ID了。
如图
二、项目代码说明
在正式环境中只要涉及到插入数据
的操作都被该插件拦截,并发量会很大。所以该插件代码即要保证线程安全
又要保证高可用
。所以在代码设计上做一些说明。
1、线程安全
这里的线程安全主要是考虑产生雪花ID的时候必须是线程安全的,不能出现同一台服务器同一时刻出现了相同的雪花ID,这里是通过
静态内部类单例模式 + synchronized
来保证线程安全的,具体有关生成雪花ID的代码这里就不粘贴。
2、高可用
我们去思考消耗性能比较大的地方可能出要出现在两个地方
1)雪花算法生成雪花ID的过程。
2)通过类的反射机制找到哪些属性带有@AutoId注解的过程。
第一点
其实在静态内部类实现雪花算法
这篇博客已经简单测试过,生成20万条数据,大约在1.7秒能满足实际开发中我们的需要。
第二点
这里是有比较好的解决方案的,可以通过两点去改善它。
1)、在插件中添加了一个Map处理器
/**
* key值为Class对象 value可以理解成是该类带有AutoId注解的属性,只不过对属性封装了一层。
* 它是非常能够提高性能的处理器 它的作用就是不用每一次一个对象经来都要看下它的哪些属性带有AutoId注解
* 毕竟类的反射在性能上并不友好。只要key包含该Class,那么下次同样的class进来,就不需要检查它哪些属性带AutoId注解。
*/
private Map<Class, List<Handler>> handlerMap = new ConcurrentHashMap<>();
插件部分源码
public class AutoIdInterceptor implements Interceptor {
/**
* Map处理器
*/
private Map<Class, List<Handler>> handlerMap = new ConcurrentHashMap<>();
/**
* 某某方法
*/
private void process(Object object) throws Throwable {
Class handlerKey = object.getClass();
List<Handler> handlerList = handlerMap.get(handlerKey);
//先判断handlerMap是否已存在该class,不存在先找到该class有哪些属性带有@AutoId
if (handlerList == null) {
handlerMap.put(handlerKey, handlerList = new ArrayList<>());
// 通过反射 获取带有AutoId注解的所有属性字段,并放入到handlerMap中
}
//为带有@AutoId赋值ID
for (Handler handler : handlerList) {
handler.accept(object);
}
}
}
2)添加break label(标签)
这个就比较细节了,因为上面的process方法
不是线程安全的,也就是说可能存在同一时刻有N个线程进入process方法,那么这里可以优化如下:
//添加了SYNC标签
SYNC:
if (handlerList == null) {
//此时handlerList确实为null,进入这里
synchronized (this) {
handlerList = handlerMap.get(handlerKey);
//但到这里发现它已经不是为null了,因为可能被其它线程往map中插入数据,那说明其实不需要在执行下面的逻辑了,直接跳出if体的SYNC标签位置。
//那么也就不会执行 if (handlerList == null) {}里面的逻辑。
if (handlerList != null) {
break SYNC;
}
}
}
这里虽然很细节,但也是有必要的,毕竟这里并发量很大,这样设计能一定程度提升性能。