mybatis的本質和原理

背景

    項目需要,我們需要自己做一套mybatis,或者使用大部分mybatis的原始內容。對其改造,以適應需要。這就要求我再次學習一下mybatis,對它有更深入的了解。

 

是什麼
    MyBatis是一個持久層框架,用來處理對象關係映射。說白了就是以相對面向對象的方式來提交sql語句給jdbc。如果想找個簡單、快速上手的例子,最好是和spring想結合的。直接用官網的吧,簡單清晰也沒誰了://mybatis.org/spring/getting-started.html

//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學習場景工程//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上,地址:

//github.com/xiexiaojing/yuna。

    很多原理性的東西看過之後會忘,但是如果真正站在設計者角度實現過一個簡化的版本,相信會增強記憶。同時也能和真正的實現做對比,更深層學習技術大牛們的設計精華

 

推薦閱讀

        技術方案設計的方法
        架構-穩定性建設邏輯問題實戰總結
        程式碼榮辱觀-以運用風格為榮,以隨意編碼為恥
        技術境界的二三四