背景
项目需要,我们需要自己做一套mybatis,或者使用大部分mybatis的原始内容。对其改造,以适应需要。这就要求我再次学习一下mybatis,对它有更深入的了解。
是什么
MyBatis是一个持久层框架,用来处理对象关系映射。说白了就是以相对面向对象的方式来提交sql语句给jdbc。如果想找个简单、快速上手的例子,最好是和spring想结合的。直接用官网的吧,简单清晰也没谁了:http://mybatis.org/spring/getting-started.html
https://mybatis.org/mybatis-3/getting-started.html
为什么
Java开发都是面向对象的思维,如果用传统下面自己去调用连接拼装sql的方式,维护成本高,代码可读性差。
public static void main(String[] args) {
//数据库连接对象
Connection conn = null;
//数据库操作对象
PreparedStatement stmt = null;
//1、加载驱动程序
try {
Class.forName(DBDRIVER);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//2、连接数据库
//通过连接管理器连接数据库
try {
//在连接的时候直接输入用户名和密码才可以连接
conn = DriverManager.getConnection(DBURL, USERNAME, PASSWORD);
} catch (SQLException e) {
e.printStackTrace();
}
//3、向数据库中插入一条数据
String sql = "INSERT INTO person(name,age) VALUES (?,?)";
try {
stmt = conn.prepareStatement(sql);
stmt.setString(1,"陈昆仑");
stmt.setInt(2,21);
stmt.executeQuery();
} catch (SQLException e) {
e.printStackTrace();
}
//4、执行语句
try {
ResultSet resultSet = stmt.executeQuery();
} catch (SQLException e) {
e.printStackTrace();
}
//5、关闭操作,步骤相反哈~
try {
stmt.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
怎么做
我们来看一下底层是怎么处理和交互的。基本流程如下:
看着头大?没事,我们先从最简化的版本开始添枝加叶。MyBatis可以用配置文件或者注解形式注入sql。因为配置文件方式可以方便的处理动态SQL(动态SQL就是sql语句里有if else for这些的,可以根据参数的变化最终sql也跟着变化)等优点,用的更为普遍。
假设现在是2000年,Clinton Begin还没有发起ibatis(mybatis的前身)项目。而apache基金会内部发起了讨论要设计这样一个产品,指派你作为项目负责人。现在思考,你的思路是什么?
一般思路是先把架构搭建起来,做成一个MVP最小可行性版本,然后再做功能增强。
从功能最简化方面来看,需要两步:第一步要将sql及所需要的元素以对象的形式输入,第二步是获取到这些信息转换成jdbc信息处理。
这样拆解后的思路是将sql及所需要的元素拆解成类方法的参数形式,方法本身要做的事情就是将这些参数以jdbc编程需要的形式传给jdbc执行。这里方法内部做的事情是一样的,那就自然而然的想到不用每个类都有一个实现。只要定义好接口,把实现用代理或者上层切面的方式统一处理就可以了。
根据这个思路,首先要用代理来获取参数。我设计使用方式是Insert、Select等注解里写sql元语句。通过方法参数注入参数。最终返回结果。如下
public interface UserMapper {
@Insert("INSERT INTO person(name,age) VALUES (#{name},#{age})")
Integer insertUser(User user);
}
要实现接口的解析。先建立一个类,里面构造一个代理类,实现类似于SqlSession,所以起名叫YunaSession(yuna是我给经典java学习场景工程https://github.com/xiexiaojing/yuna 起的名字)
public class YunaSession {
public static Object dealSql(Class clazz) {
Class c[] = new Class[]{clazz};
return Proxy.newProxyInstance(YunaSession.class.getClassLoader(), c,
new YunaInvocationHandler());
}
}
下面要实现的是代理中YunaInvocationHandler真正要实现的逻辑:将这些参数以jdbc编程需要的形式传给jdbc执行。也就是说把上面【为什么】部分一开始的那段执行jdbc的代码贴进去,将sql和参数的部分做替换。
我们把关键再贴一遍便于说明问题
//3、向数据库中插入一条数据
String sql = "INSERT INTO person(name,age) VALUES (?,?)";
try {
stmt = conn.prepareStatement(sql);
stmt.setString(1,"陈昆仑");
stmt.setInt(2,21);
stmt.executeQuery();
} catch (SQLException e) {
e.printStackTrace();
}
这里有两个?,而jdbc的预处理语句传入参数的时候要明确的知道第一个参数的类型是什么,如果传过来是对象的话,要知道对应对象的哪个值。这就是为什么接口里的预处理语句传入是
INSERT INTO person(name,age) VALUES (#{name},#{age})
因为可以通过匹配#{XX}这样的确定都是哪些参数,因为User对象里有定义参数的类型。所以类型和值都确定了。这个就是MappedStatement对象做的事情。以下是用正则表达式匹配+反射来达到解析sql并和对象值做匹配的实现:
public static void main(String[] args) throws Exception{
Matcher m= pattern.matcher("INSERT INTO person(name,age) VALUES (#{name},#{age})");
User user1 = new User();
user1.setId(1);
user1.setName("贾元春");
user1.setAge(27);
int i=1;
while(m.find()) {
System.out.println(m.group());
String group = m.group();
String fieldName = group.replace("#{","").replace("}","");
Field field = User.class.getDeclaredField(fieldName);
field.setAccessible(true);
if("java.lang.Integer".equals(field.getType().getName())) {
System.out.println("stmt.setInt("+i+","+field.get(user1)+")");
} else if("java.lang.String".equals(field.getType().getName())) {
System.out.println(" stmt.setString("+i+","+field.get(user1)+")");
}
i++;
}
}
运行结果是
可以看到实现了效果。下面就是和jdbc连接结合起来。
public class YunaInvocationHandler implements InvocationHandler {
public static final String DBDRIVER = "org.xx.mm.mysql.Driver";
public static final String DBURL = "jdbc:mysql://localhost:3306/mydb";
//现在使用的是mysql数据库,是直接连接的,所以此处必须有用户名和密码
public static final String USERNAME = "root";
public static final String PASSWORD = "mysqladmin";
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception{
Object result = null;
Insert insert = method.getAnnotation(Insert.class);
if (insert != null) {
String sql = insert.value()[0];
System.out.println("插入语句为"+s);
YunaSqlDeal yunaSqlDeal = new YunaSqlDeal();
yunaSqlDeal.insert(s, Arrays.toString(args));
//1、加载驱动程序
try {
Class.forName(DBDRIVER);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//2、连接数据库
//通过连接管理器连接数据库
//数据库连接对象
Connection conn = null;
try {
//在连接的时候直接输入用户名和密码才可以连接
conn = DriverManager.getConnection(DBURL, USERNAME, PASSWORD);
} catch (SQLException e) {
e.printStackTrace();
}
composeStatement(sql, args[0], conn);
}
return 1;
}
private static final String PATTERN = "#\{[A-Za-z0-9]+\}";
private static Pattern pattern = Pattern.compile("("+PATTERN+")");
public static void composeStatement(String sql, Object obj, Connection conn) throws Exception{
PreparedStatement stmt = conn.prepareStatement(sql.replaceAll(PATTERN, ""));
Matcher m= pattern.matcher(sql);
int i=1;
while(m.find()) {
System.out.println(m.group());
String group = m.group();
String fieldName = group.replace("#{","").replace("}","");
Field field = User.class.getDeclaredField(fieldName);
field.setAccessible(true);
if("java.lang.Integer".equals(field.getType().getName())) {
System.out.println("stmt.setInt("+i+","+field.get(obj)+")");
stmt.setInt(i, Integer.parseInt(field.get(obj).toString()));
} else if("java.lang.String".equals(field.getType().getName())) {
stmt.setString(i, field.get(obj).toString());
}
i++;
}
stmt.execute();
stmt.close();
conn.close();
}
}
这个实现的是insert的,返回值类型固定,如果是select查询语句,涉及到返回的结果封装成对象。思路也是通过反射,和参数转换步骤差不多,就不贴代码了。
到此,我们实现了一个简化版的mybatis框架。比贴的架构图简化在少用了很多设计模式的东西,和出于性能考虑重用的东西。mybatis的核心就实现完了。
总结
本文从mybatis的设计者角度出发,构造了一个简化的mybatis框架。具体可运行的完整代码放到了我的github上,地址:
https://github.com/xiexiaojing/yuna。
很多原理性的东西看过之后会忘,但是如果真正站在设计者角度实现过一个简化的版本,相信会增强记忆。同时也能和真正的实现做对比,更深层学习技术大牛们的设计精华
推荐阅读