1.Mapper代理模式的特点
程序员没有写接口的子实现——直接获取数据库的数据
因为Mybatis定义了一套规则,对方法进行了实现,程序员只要遵循这套方法就可以直接使用
2.如何实现Mapper代理模式
步骤:
1.创建一个dao接口,在接口中写增删改查的方法
2.创建一个子清单文件,且子清单文件中的命名空间必须namespace="包名.接口名"
3.在子清单文件中:select\insert\update\delete 节点id为接口中的方法名称
方法的参数对应parameterType,返回值对应resultType
4.在总清单文件引入子清单文件
5.在需要的地方:接口类型 jdk动态代理对象=sqlSqssion.getMapper(接口类型.class)
返回类型 返回类型对象 jdk动态代理对象.调用目标方法();
程序员没有写接口子实现,就能获得数据库数据
configruation.xml:
设置类别名,方便书写:
<typeAliases>
<!--设置别名-->
<typeAlias type="org.example.entity.User" alias="User"/>
</typeAliases>
注意添加子清单文件
<mapper resource="mapper/userMapper.xml"/>
增删改实现
xxMapper.java
public int addUser(User user);
public int deleteUser(Integer id);
public int updateUser(User user);
userMapper.xml
<mapper namespace="org.example.dao.UserMapper">
<!--插入用户-->
<insert id="addUser"
parameterType="User">
insert into t_user(
user_name,
user_password,
address)
values(
#{name},
#{password},
#{address}
);
</insert>
<!--删除用户-->
<delete id="deleteUser"
parameterType="java.lang.Integer">
delete from t_user
where id=#{id};
</delete>
<!--根据id更新用户-->
<update id="updateUser"
parameterType="org.example.entity.User">
update t_user set
user_name=#{name},
user_password=#{password},
address=#{address}
where
id=#{id}
</update>
</mapper>
单元测试:
@Test
public void addUser() {
//假数据
User user=new User();
user.setName("add");
user.setPassword("654321");
user.setAddress("测试用例|mapper.addUser()");
Integer rowAffect=0;
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
rowAffect= userMapper.addUser(user);
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
System.out.println(rowAffect);
}
@Test
public void deleteUser() {
Integer rowAffect=0;
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
rowAffect= userMapper.deleteUser(9);
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
System.out.println(rowAffect);
}
@Test
public void updateUser() {
//假数据
User user=new User();
user.setId(9);
user.setName("update");
user.setPassword("654321");
user.setAddress("测试用例|mapper.updateUser()");
Integer rowAffect=0;
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
rowAffect= userMapper.updateUser(user);
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
System.out.println(rowAffect);
}
效果展示:
addUser
updateUser
deleteUser
总结:
mybatis 的 mapper代理模式和原生api相比,可以不写接口的具体实现类。
mapper模式是纯接口调用,因为有接口的存在,可以使用jdk动态代理为其动态生成实现类。
jdk生成的实现类和接口之间是实现和被实现的关系,
jdk生成的实现类是实现类,也是代理类,其创建动态代理对象,通过代理对象.目标方法的方式,即invacationHandler调用invoke(),来进行实现。
查询:根据id获取用户信息
xxMapper.java
public User findUserById(Integer id);
userMapper.xml
<!--根据id获得用户信息-->
<select id="findUserById"
resultType="org.example.entity.User"
parameterType="java.lang.Integer">
select
id,
user_name as name,
user_password as password,
address
from t_user
where id = #{id}
</select>
单元测试:
@Test
public void findUserById() {
User user=null;
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
user= userMapper.findUserById(1);
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
System.out.println(user);
}
结果展示:
查询:根查所有用户List
xxMapper.java
//返回list结构
public List<User> findAllUser1();
public List<Map<String,Object>> findAllUser2();
userMapper.xml
<!--查所有返回list<User>-->
<select id="findAllUser1"
resultType="User"
>
select
id,
user_name as name,
user_password as password,
address
from t_user
</select>
<!--查所有返回list<Map>-->
<select id="findAllUser2"
resultType="java.util.Map"
>
select
id,
user_name,
user_password,
address
from t_user
</select>
单元测试:
@Test
public void findAllUser1() {
List<User> userList=new ArrayList<User>();
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
userList= userMapper.findAllUser1();
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
for(User user:userList){
System.out.println(user);
}
}
@Test
public void findAllUser2() {
List<Map<String,Object>> userList=new ArrayList<Map<String, Object>>();
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
userList= userMapper.findAllUser2();
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
for(Map<String,Object> user:userList){
System.out.println(user);
}
}
结果展示:
查询:根查所有用户Map
xxMapper.java
//返回map结构
@MapKey("id")
public Map<Integer,User> findAllUser3();
@MapKey("id")
public Map<Integer,Map<String,Object>> findAllUser4();
userMapper.xml
<!--查所有返回Map<User>-->
<select id="findAllUser3"
resultType="User"
>
select
id,
user_name as name,
user_password as password,
address
from t_user
</select>
<!--查所有返回Map<Map>-->
<select id="findAllUser4"
resultType="java.util.Map"
>
select
id,
user_name,
user_password,
address
from t_user
</select>
单元测试:
@Test
public void findAllUser3() {
Map<Integer,User> userMap= new HashMap<Integer, User>();
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
userMap= userMapper.findAllUser3();
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
for(Integer key:userMap.keySet()){
System.out.println(key+" "+userMap.get(key));
}
}
@Test
public void findAllUser4() {
Map<Integer,Map<String,Object>> userMap= new HashMap<Integer, Map<String,Object>>();
SqlSession sqlSession=null;
try{
sqlSession= MybatisUtil.getSession();
//接口类型, jdk动态代理对象=sqlSession.getMapper();
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
userMap= userMapper.findAllUser4();
sqlSession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
}finally {
if(sqlSession!=null){
sqlSession.close();
}
}
for(Integer key:userMap.keySet()){
System.out.println(key+" "+userMap.get(key));
}
}
结果展示:
3.JDKProxy模拟Mapper代理实现
Mybatis的Mapper代理模式,可以通过接口使用jdk的代理机制实现自动代理。(jdk的动态代理是基于接口的,而此处的xxMapper接口即为被代理的对象接口)
本质上xxMapper并没有实现类,jdk的动态代理也没有对所谓的老方法进行代理。
程序员不需要写接口的具体实现,只要根据规则定义好接口,即可使用相关功能。
我们使用JDKProxy类模拟Mybatis实现getMapper()的方法,理解其中的内部实现。
package org.example.proxy;
import org.example.proxy.handler.MapperHandler;
import java.lang.reflect.Proxy;
public class JDKProxy {
/**
* 根据接口获取代理对象
* @param clazz
* @return
*/
public static Object getMapper(Class clazz){
Object proxyObject=null;
/**
* 参数一: 类加载器
* 参数二: 接口数组
* 参数三: InvocationHandler 接口的回调
*/
proxyObject= Proxy.newProxyInstance(
clazz.getClassLoader(),
new Class[]{clazz},
new MapperHandler()
);
return proxyObject;
}
}
这里虽然使用了动态代理,但是并不是动态代理的常规用法,这里只有代理对象的接口,且该接口没有任何实现。动态代理在这里的目的并不是调用老方法做切面设计,而是获取方法信息:参数列表、返回值等,进行判断,根据方法选择调用mybatis的原生api。所以Mapper代理模式底层调用的还是Mybatis的原生api, 其只是通过动态代理实现了一个默认实现版本,来较少程序员的代码量。
关于JDK的第三个参数 InvocationHandler 接口的回调,我们构建实现类MapperHandler,其实现InvocationHandler接口。具体来说就是:
package org.example.proxy.handler;
import org.example.entity.User;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MapperHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue=null;
System.out.println("由Mybatis根据若干信息选择执行对应的api");
/**
* mybatis 根据method能得到目标方法的返回值,方法名,参数类型
* if判断返回的数据为一个值,则调用mybatis的原生api selectOne方法
* if判断返回的数据为多个值时,看返回类型是否为List,则调用mybatis的原生 selectList()方法
* if判断返回的数据为多个值时,看返回类型是否为Map,则调用mybatis的原生 selectMap()方法
* if判断为insert/delete/update时,调用原生api insert() delete() update()
*/
//eg:执行selectOne()
User user=new User();
user.setId(1);
user.setName("aaa");
user.setPassword("123456");
user.setAddress("诺亚方舟");
returnValue=user;
return returnValue;
}
}
使用单元测试来对方法进行测试:
package org.example.proxy;
import org.example.dao.UserMapper;
import org.example.entity.User;
import org.junit.Test;
import static org.junit.Assert.*;
public class JDKProxyTest {
@Test
public void getMapper() {
//接口类型 jdk动态代理对象=sqlSession.getMapper(接口类型.class);
UserMapper userMapper=(UserMapper) JDKProxy.getMapper(UserMapper.class);
User user=userMapper.findUserById(1);
System.out.println(user);
}
}
值得注意的是,getMapper中我们并没有写具体的实现逻辑,因为测试的是findUserById()方法,我们在getMapper()中返回了一个假数据,用来做测试。而Mybatis则根据逻辑写了具体的实现。
4.Mybatis 中getMapper的实现
重点:模拟getMapper的实现
public Object execute(SqlSession sqlSession, Object[] args) {
Object param;
Object result;
switch (this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
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 {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
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;
}
}
基本流程:getMapper()接口——>创建Proxy对象——>execute()实现具体实现
特别的关于select()方法
其中method.hasResultHandler()来实现自定义的方法处理
还可以匹配不同的返回类型,其中游标模式Cursor不怎么用了
5.总结
接口定义:userDao 和UserMapper一样的写法不同,用来定义方法接口
其可以自己写实现类也可以通过mapper() 方式来操作数据
总清单文件:
数据库配置
子清单文件
模板统一
原生写法的子清单文件 namespace和增删改查节点信息任意配置
mapper写法的子清单 namespace="包名.接口"
接口方法名和节点id一致
接口方法参数和节点parameterType一样
接口的返回值跟节点resultType一样
mybatis自己的api解析xml来构建sqlSessionFactory,用于生产SqlSession
使用sqlSession来做增删改查
mapper接口方法底层还是原生api api结合jdk动态代理
原生api
- insert() delete() update()
- selectOne() selectList() selectMap()
- select() 自定义返回的数据结构(策略+回调)
- 返回结果类型丰富
6.设计模式:
代理模式:MapperProxy实现jdk动态代理
7.补充
什么是面向接口编程?
如select面向接口、jdbc面向接口和spring面向接口都有相关的面向对象接口编程,其目的是实现解耦操作
- 面向接口编程是一种编程范式,它强调的是在设计软件应用时,应该先定义接口,然后再实现接口。这种方式有很多优点,包括提高代码的可读性、可维护性和可扩展性,以及降低代码之间的耦合度。
- 接口是一种契约,它定义了一组方法,这些方法应该在实现接口的类中实现。接口本身并不包含任何实现细节,它只是定义了一种规范,规定了实现接口的类应该做什么,而不是怎么做。
摘自:《17.Spring 面向接口编程 - 知乎》
select()也是面向接口编程提供select接口但没有实现,其具体的实现在handler中去处理。实现了接口和实现的分离。