框架源碼系列十二:Mybatis源碼之手寫Mybatis

1、需求分析

一、Mybatis是什麼?

一個半自動化的orm框架(Object Relation Mapping)。java

二、Mybatis完成什麼工做?

在面向對象編程中,咱們操做的都是對象,Mybatis框架是一個數據訪問層的框架,幫咱們完成對象在數據庫中的
存、取工做。正則表達式

爲何稱爲半自動化?spring

關係型數據庫的操做是經過SQL語句來完成的,Mybatis在幫咱們作對象的存取時,須要咱們提供對應的SQL語句,它不自動幫咱們生成SQL語句,而只幫咱們完成:sql

1)對象屬性到SQL語句參數的自動填充;
2)SQL語句執行結果集到對象的自動提取;
因此稱爲半自動的。而咱們瞭解的另外一個ORM框架Hibernate則是全自動的。數據庫

半自動化的不足:咱們得辛苦一點編寫SQL語句。
半自動化的優勢:咱們能夠徹底把控執行的SQL語句,能夠隨時靈活調整、優化。編程

三、爲何要用Mybatis?

1)mybatis學習、使用簡單
2)半自動化的優勢數組

四、爲何要學好Mybatis?

一線互聯網公司出於性能、調優、使用簡單、徹底可控的須要,在數據庫訪問層都是採用Mybatis。安全

5 、爲何要用orm框架?

都是爲了提升生產效率,少寫代碼,少寫重複代碼!
不用orm框架,能用什麼來完成數據的存取?
jdbc
看看jdbc編程的代碼示例session

package com.study.leesmall.sample.mybatis.jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.study.leesmall.sample.mybatis.model.User;

@Component
public class UserDao {

    @Autowired
    private DataSource dataSource;

    public void addUser(User user) throws SQLException {

        try (
                // 一、獲取鏈接
                Connection conn = DataSourceUtils.getConnection(dataSource);
                // 二、建立預編譯語句對象
                PreparedStatement pst = conn.prepareStatement(
                        "insert into t_user(id,name,sex,age,address,phone,wechat,email,account,password) "
                                + " values(?,?,?,?,?,?,?,?,?,?)");) {
            // 三、設置參數值
            int i = 1;
            pst.setString(i++, user.getId());
            pst.setString(i++, user.getName());
            pst.setString(i++, user.getSex());
            pst.setInt(i++, user.getAge());
            pst.setString(i++, user.getAddress());
            pst.setString(i++, user.getPhone());
            pst.setString(i++, user.getWechat());
            pst.setString(i++, user.getEmail());
            pst.setString(i++, user.getAccount());
            pst.setString(i++, user.getPassword());

            // 四、執行語句
            int changeRows = pst.executeUpdate();
        }
    }

    public List<User> queryUsers(String likeName, int minAge, int maxAge, String sex) throws SQLException {
        // 一、根據查詢條件動態拼接SQL語句
        StringBuffer sql = new StringBuffer(
                "select id,name,sex,age,address,phone,wechat,email,account,password from t_user where 1 = 1 ");
        if (!StringUtils.isEmpty(likeName)) {
            sql.append(" and name like ? ");
        }

        if (minAge >= 0) {
            sql.append(" and age >= ? ");
        }

        if (maxAge >= 0) {
            sql.append(" and age <= ? ");
        }

        if (!StringUtils.isEmpty(sex)) {
            sql.append(" and sex = ? ");
        }

        try (Connection conn = DataSourceUtils.getConnection(dataSource);
                PreparedStatement pst = conn.prepareStatement(sql.toString());) {
            // 2 設置查詢語句參數值
            int i = 1;
            if (!StringUtils.isEmpty(likeName)) {
                pst.setString(i++, "%" + likeName + "%");
            }

            if (minAge >= 0) {
                pst.setInt(i++, minAge);
            }

            if (maxAge >= 0) {
                pst.setInt(i++, maxAge);
            }

            if (!StringUtils.isEmpty(sex)) {
                pst.setString(i++, sex);
            }

            // 3 執行查詢
            ResultSet rs = pst.executeQuery();

            // 四、提取結果集
            List<User> list = new ArrayList<>();
            User u;
            while (rs.next()) {
                u = new User();
                list.add(u);
                u.setId(rs.getString("id"));
                u.setName(rs.getString("name"));
                u.setSex(rs.getString("sex"));
                u.setAge(rs.getInt("age"));
                u.setPhone(rs.getString("phone"));
                u.setEmail(rs.getString("email"));
                u.setWechat(rs.getString("wechat"));
                u.setAccount(rs.getString("account"));
                u.setPassword(rs.getString("password"));
            }

            rs.close();

            return list;
        }
    }
}

用JdbcTemplate的代碼示例:mybatis

package com.study.leesmall.sample.mybatis.jdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.util.StringUtils;

import com.study.leesmall.sample.mybatis.model.User;

//@Component
public class UserDaoUseJdbcTemplate {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void addUser(User user) throws SQLException {
        String sql = "insert into t_user(id,name,sex,age,address,phone,wechat,email,account,password) "
                + " values(?,?,?,?,?,?,?,?,?,?)";
        jdbcTemplate.update(sql, user.getId(), user.getName(), user.getSex(), user.getAge(), user.getAddress(),
                user.getPhone(), user.getWechat(), user.getEmail(), user.getAccount(), user.getPassword());
    }

    public List<User> queryUsers(String likeName, int minAge, int maxAge, String sex) throws SQLException {
        // 一、根據查詢條件動態拼接SQL語句
        StringBuffer sql = new StringBuffer(
                "select id,name,sex,age,address,phone,wechat,email,account,password from t_user where 1 = 1 ");
        List<Object> argList = new ArrayList<>();
        if (!StringUtils.isEmpty(likeName)) {
            sql.append(" and name like ? ");
            argList.add("%" + likeName + "%");
        }

        if (minAge >= 0) {
            sql.append(" and age >= ? ");
            argList.add(minAge);
        }

        if (maxAge >= 0) {
            sql.append(" and age <= ? ");
            argList.add(maxAge);
        }

        if (!StringUtils.isEmpty(sex)) {
            sql.append(" and sex = ? ");
            argList.add(sex);
        }

        return jdbcTemplate.query(sql.toString(), argList.toArray(), new RowMapper<User>() {
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                User u = new User();
                u.setId(rs.getString("id"));
                u.setName(rs.getString("name"));
                u.setSex(rs.getString("sex"));
                u.setAge(rs.getInt("age"));
                u.setPhone(rs.getString("phone"));
                u.setEmail(rs.getString("email"));
                u.setWechat(rs.getString("wechat"));
                u.setAccount(rs.getString("account"));
                u.setPassword(rs.getString("password"));

                return u;
            }
        });
    }
}

參數設置代碼、結果集處理代碼、JDBC過程代碼都會大量重複,毫無技術含量!
那就寫個框架作了它!顯示咱們的牛B!

六、框架確切需求

1)用戶只需定義持久層接口(dao接口)、接口方法對應的SQL語句。
2)用戶需指明接口方法的參數與SQL語句參數的對應關係。
3)用戶需指明SQL查詢結果集與對象屬性的映射關係。
4)框架完成接口對象的生成,JDBC執行過程

2、設計 

一、需求 1

1)用戶只需定義持久層接口(dao接口)、接口方法對應的SQL語句。

設計問題:
1)咱們該提供什麼樣的方式來讓用戶定義SQL語句?
2)SQL語句怎麼與接口方法對應?
3)這些SQL語句、對應關係咱們框架須要獲取到,誰來獲取?又該如何表示存儲

1.1 SQL定義方式

XML方式:獨立於代碼,修改很方便(不需改代碼)
註解方式:直接加在方法上,零xml配置

問題:SQL語句可作增、刪、改、查操做,咱們是否要對SQL作個區分?
答:要,由於jdbc中對應有不一樣的方法 executeQuery executeUpdate

xml方式定義SQL語句定義方式 :設計增刪改查的元素:

<!ELEMENT insert(#PCDATA) >
<!ELEMENT update(#PCDATA) >
<!ELEMENT delete(#PCDATA) >
<!ELEMENT select (#PCDATA) > 
<insert>insert into t_user(id,name,sex,age) values(?,?,?,?)</insert>

註解方式定義SQL語句定義方式 :設計增刪改查的註解:@Insert @Update @Delete @Select ,註解項定義SQL

@Documented
@Retention(RUNTIME)
@Target({ METHOD })
public @interface Insert {
    String value();
}
@Insert("insert into t_user(id,name,sex,age) values(?,?,?,?)")
public void addUser(User user);

 1.2 SQL語句與接口方法對應

 xml方式時,如何來映射SQL語句對應的接口方法?

 爲元素定義一個id,id的值爲對應的類名.方法名,如何?

<insert id="com.study.leesmall.sample.UserDao.addUser">
         insert into t_user(id,name,sex,age) values(?,?,?,?)
</insert>

一個Dao接口中可能會定義不少個數據訪問方法,id這麼寫很長,能不能便捷一點?
這是在作SQL與接口方法的映射,咱們來加一個mapper元素,它可包含多個insert、update、delete、select元素,至關於分組,一個接口中定義的分到一組。
在mapper中定義一個屬性namespace,指定裏面元素的名稱空間,namespace的值對應接口類名,裏面元素的id對應方法名。

mybatis-mapper.dtd

<!ELEMENT mapper (insert* | update* | delete* | select*)+ >
<!ATTLIST mapper namespace CDATA #IMPLIED >
<!ELEMENT insert(#PCDATA) >
<!ELEMENT update(#PCDATA) >
<!ELEMENT delete(#PCDATA) >
<!ELEMENT select (#PCDATA) >

 這個xml文件命名爲 userDaoMapper.xml,內容以下:

<mapper namespace="com.study.leesmall.sample.userDao">
    <insert id="addUser">
        insert into t_user(id,name,sex,age) values(?,?,?,?)
    </insert>
</mapper>

1.3 映射關係的獲取與表示、存儲

xml方式:
       解析xml來獲取
註解方式:
      讀取註解信息

問題:

1) 怎麼表示?
得設計一個類來表示從xml、註解得到的SQL映射信息。

注意:

id爲惟一id:
xml方式:id=namespace.id屬性值
註解方式:id=完整類名.方法名
2) 怎麼存儲獲得的MappedStatement?
這些其實就是一個配置信息,咱們定義一個Configuration類:

 

注意:key 爲MappedStatement的id

3) 得有類來負責解析xml

 

XmlMapperBuilder負責解析xml文檔(parse方法的resource參數用來指定inputStream的來源),parse方法它調用XMLStatementBuilder來解析裏面的parse方法,解析完成之後,把得到的信息存儲到Configuration裏面

 4)mapper中可讓用戶如何來指定文件位置?

文件能夠是在類目錄下,也但是在文件系統目錄下。如何區分?
規定:
類目錄下的方式經過 resource屬性指定;
文件系統文件經過 url屬性指定,值採用URL 本地文件格式指定:file:///

<configuration>
    <mappers>
        <mapper resource="com/leesmall/UserMapper.xml"/>
        <mapper url="file:///var/mappers/CourseMapper.xml"/>
    <mappers>
</configuration>

 定義 mybatis-config.dtd

<!ELEMENT configuration (mappers?)+ >
<!ELEMENT mappers (mapper*)>
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
> 

5) 增長了一個config xml文件,就的有類來解析它。

增長解析mybatis-config.xml配置文件的類

 

6) 註解的方式須要獲取SQL映射信息,也得有個類來作這件事

 

7) 誰來使用MapperAnnotationBuilder?

 Configuration吧,在它裏面持有MapperAnnotationBuilder,增長添加Mapper接口類的方法。

 

8) 用戶如何來指定他們的Mapper接口類?

 一、在mybatis-config.xml的mappers中經過mapper指定?

<configuration>
    <mappers>
        <mapper resource="com/leesmall/UserMapper.xml"/>
        <mapper url="file:///var/mappers/CourseMapper.xml"/>
    <mappers>
</configuration>

如何來區分它是個Mapper接口呢?

給mapper加一個屬性class來專門指定Mapper類名

<configuration>
    <mappers>
        <mapper resource="com/leesmall/UserMapper.xml"/>
        <mapper url="file:///var/mappers/CourseMapper.xml"/>
        <mapper class="com.study.leesmall.dao.UserDao" />
    <mappers>
</configuration>

 mybatis-config.dtd

<!ELEMENT configuration (mappers?)+ >
<!ELEMENT mappers (mapper*)>
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>

問題:
一、這樣一個一個類來指定,好繁瑣?能不能指定一個包名,包含包下全部接口、子孫包下的接口類?
二、包含包下全部的接口,好像不是很靈活,能不能讓用戶指定包下全部某類型的接口?
如是什麼類型的類,或帶有某註解的接口。
好的,這很容易,在mappers元素中增長一個package元素,pacakge元素定義三個屬性

mybatis-config.dtd

<!ELEMENT configuration (mappers?)+ >

<!ELEMENT mappers (mapper*,package*)>

<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>

<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #IMPLIED
type CDATA #IMPLIED
annotation CDATA #IMPLIED
>
<configuration>
    <mappers>
        <mapper resource="com/leesmall/UserMapper.xml"/>
        <mapper url="file:///var/mappers/CourseMapper.xml"/>
        <mapper class="com.study.leesmall.dao.UserDao" />
        <package name="com.study.leesmall.mapper" />
        <package name="com.study.leesmall.mapper" type="com.study.leesmall.MapperInterface"/>
        <package name="com.study.leesmall.mapper"
                      annotation="com.study.leesmall.mybtis.annotation.Mapper"/>
        <package name="com.study.leesmall.mapper" type="com.study.leesmall.MapperInterface"
                      annotation="com.study.leesmall.mybtis.annotation.Mapper"/>
    <mappers>
</configuration>

爲了用戶使用方便,咱們給定義一個@Mapper註解,默認規則:指定包下加了@Mapper註解的接口,如何?

加了package元素,又得在Configuration中增長對應的方法了:

 

約定俗成的規則:指定包下掃到的@Mapper接口,例如UserDao,還能夠在包下定義 UserDao.xml,會被加載解析。

2 需求2

需求二、用戶需指明接口方法的參數與語句參數的對應關係。

2.1 語句參數指定

 看下面的Mapper示例

@Mapper
public interface UserDao {
    @Insert("insert into t_user(id,name,sex,age) values(?,?,?,?)")
    void addUser(User user);
}

User對象的屬性如何與 values(?)對應?
靠解析 t_user(id,name,sex,age) 可行嗎?
難度太大!
萬一User的name叫xname呢!
既然靠咱們來解析不行,那就請用戶指明吧。用戶如何來指明呢?

咱們來給定個規則: ? 用 #{屬性名} 代替,咱們來解析SQL語句中的 #{屬性名} 來決定參數對應。

@Insert("insert into t_user(id,name,sex,age) values(#{id},#{name},#{sex},#{age})")
void addUser(User user);

 萬一是這種狀況呢?

@Insert("insert into t_user(id,name,sex,age) values(#{id},#{name},#{sex},#{age})")
void addUser(String id,String xname,String sex,int age);

徹底能夠要求用戶必須與參數名對應 :#{xname}。
爲了提升點自由度(及後面方便SQL複用),能夠定義一個註解讓用戶使用,該註解只可用在參數上

 

@Insert("insert into t_user(id,name,sex,age) values(#{id},#{name},#{sex},#{age})")
void addUser(String id,@Param("name")String xname,String sex,int age);

 萬一是這種狀況呢?

@Insert("insert into t_user(id,name,sex,age,org_id) values(#{id},#{name},#{sex},#{age},#{id})")
void addUser(User user,Org org);

 User和Org中都有id屬性,name屬性

 

好辦,若是方法參數是對象,則以 參數名.屬性 的方式指定SQL參數:

@Insert("insert into t_user(id,name,sex,age,org_id) values(#{user.id},#{user.name},#{user.sex},#{user.age},#{org.id})")
void addUser(User user,Org org);

同樣也可使用@Param

 若是方法參數是這種狀況呢?

@Insert("insert into t_user(id,name,sex,age) values(#{id},#{name},#{sex},#{age})")
void addUser(Map map);

對應Map中的key

 若是方法參數是這種狀況呢?

@Insert("insert into t_user(id,name,sex,age,org_id) values(#{user.id},#{user.name},#{user.sex},#{user.age},#{org.id})")
void addUser(Map map,Org org);
@Insert("insert into t_user(id,name,sex,age,org_id) values(#{user.id},#{user.name},#{user.sex},#{user.age},#{org.id})")
void addUser(@Param("user")Map map,Org org);

 再來看下下面的場景:

@Select("select id,name,sex from t_user where sex = #{sex} order by #{orderColumn}")
List<User> query(String sex, String orderColumn);

order by #{orderColumn} order by ? 能夠嗎?

不能夠,也就是說 方法參數不全是用來作SQL語句的預編譯參數值的,有些是來構成SQL語句的一部分的。

那怎麼讓用戶指定呢?

 同樣,定義個規則: ${屬性名} 表示這裏是字符串替換

@Select("select id,name,sex from t_user where sex = #{sex} order by ${orderColumn}")
List<User> query(String sex, String orderColumn);

 2.2 SQL中參數映射解析

 問題:

1) SQL中參數映射解析要完成的是什麼工做?

解析出真正的SQL語句

得到方法參數與語句參數的對應關係 : 問號N---哪一個參

    public void addUser(User user) throws SQLException {

        try (
                // 一、獲取鏈接
                Connection conn = DataSourceUtils.getConnection(dataSource);
                // 二、建立預編譯語句對象
                PreparedStatement pst = conn.prepareStatement(
                        "insert into t_user(id,name,sex,age,address,phone,wechat,email,account,password) "
                                + " values(?,?,?,?,?,?,?,?,?,?)");) {
            // 三、設置參數值
            int i = 1;
            pst.setString(i++, user.getId());
            pst.setString(i++, user.getName());
            pst.setString(i++, user.getSex());
            pst.setInt(i++, user.getAge());
            pst.setString(i++, user.getAddress());
            pst.setString(i++, user.getPhone());
            pst.setString(i++, user.getWechat());
            pst.setString(i++, user.getEmail());
            pst.setString(i++, user.getAccount());
            pst.setString(i++, user.getPassword());

            // 四、執行語句
            int changeRows = pst.executeUpdate();
        }
    }

 怎麼解析?

@Insert("insert into t_user(id,name,sex,age,org_id) values(#{user.id},#{user.name},#{user.sex},#{user.age},#{org.id})")
void addUser(@Param("user")Map map,Org org);

 方式有:

正則表達式
antlr

怎麼表示?

 問號的index、值來源

 2) 這個解析的工做在什麼時候作好?誰來作好?

設計怎麼來執行一個Mapper接口了
SqlSession
SqlSessionFactory

3. 需求4

1)用戶只需定義持久層接口(dao接口)、接口方法對應的SQL語句。
2)用戶需指明接口方法的參數與語句參數的對應關係。
3)用戶需指明查詢結果集與對象屬性的映射關係。
4)框架完成接口對象的生成,JDBC執行過程。

3.1 SQL語句有了,參數對應關係也有了,咱們想一想怎麼執行SQL吧。

問題:
一、要執行SQL,咱們得要有DataSource,誰來持有DataSource?

二、誰來執行SQL? Configuration ?

不合適,它是配置對象,持有全部配置信息!
既然是來作事的,那就先定義一個接口吧:SqlSession

 

該爲它定義什麼方法呢?

 需求4:框架完成接口對象的生成,JDBC執行過程。

 用戶給定Mapper接口類,要爲它生成對象,用戶再使用這個對象完成對應的數據庫操做。

 

使用示例:

UserDao userDao = sqlSession.getMapper(UserDao.class);
userDao.addUser(user);

 來爲它定義一個實現類:DefaultSqlSession

 

它該持有什麼屬性嗎?
用戶給入一個接口類,DefaultSqlSession中就爲它生成一個對象?
萬一給入的不是一個Mapper接口呢?
也爲其生成一個對象就不合理了?
那怎麼判斷給入的接口類是不是一個Mapper接口呢?
那就只有在配置階段掃描、解析Mapper接口時作個存儲了。
存哪,用什麼存?
這也是配置信息,仍是存在Configuration 中,就用個Set來存吧。

 

DefaultSqlSession中須要持有Configuration

 

3.2 對象生成

 一、如何爲用戶給入的Mapper接口生成對象?

   很簡單,JDK動態代理

Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] {
mapperInterface }, invocationHandler);

 寫一版DefaultSqlSession的實現:

package com.study.leesmall.mybatis.session;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.study.leesmall.mybatis.config.Configuration;
public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    
    public DefaultSqlSession(Configuration configuration) {
        super();
        this.configuration = configuration;
    }
    
    @Override
    public <T> T getMapper(Class<T> type) {
        //檢查給入的接口
        if (!this.configuration.getMappers().contains(type)) {
        throw new RuntimeException(type + " 不在Mapper接口列表中!");
        }
        //獲得 InvocationHandler
        InvocationHandler ih = null; // TODO 必需要有一個
        // 建立代理對象
        T t = (T)Proxy.newProxyInstance(type.getClassLoader(), new
        Class<?>[] {type}, ih);
        return t;
    }
}

問題:每次調用getMapper(Class type)都須要生成一個新的實例嗎?

代理對象中持有InvocationHandler,若是InvocationHandler能作到線程安全,就只須要一個實例。
還得看InvocationHandler,先放這吧,把InvocationHandler搞定先

3.3 執行SQL的InvocationHandler

瞭解 InvocationHandler
InvocationHandler 是在代理對象中完成加強。咱們這裏經過它來執行SQL。

public interface InvocationHandler {
    /**
    @param proxy 生成的代理對象
    @param method 被調用的方法
    @param args
    @return Object 方法執行的返回值
    */
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;
}

來實現咱們的InvocationHandler: MapperProxy

package com.study.leesmall.mybatis.session;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MapperProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
    // TODO 這裏須要完成哪些事?
    return null;
}
}

 思考: 在 MapperProxy.invoke方法中須要完成哪些事?

package com.study.leesmall.mybatis.session;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MapperProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
        // TODO 這裏須要完成哪些事?
        // 一、得到方法對應的SQL語句
        
        // 二、解析SQL參數與方法參數的對應關係,獲得真正的SQL與語句參數值
        
        // 三、得到數據庫鏈接
        
        // 四、執行語句
        
        // 五、處理結果
        
        return null;
    }
}

一、得到方法對應的SQL語句
  要得到SQL語句,須要用到Configuration,MapperProxy中需持有Configuration

 

問題:id怎麼得來?
id是類名.方法名。從invoke方法的參數中能得來嗎?

public Object invoke(Object proxy, Method method, Object[] args)

method參數能獲得方法名,但獲得的類名不是Mapper接口類名。
那就直接讓MapperProxy持有其加強的Mapper接口類吧!簡單!

 

二、解析SQL參數與方法參數的對應關係,獲得真正的SQL與語句參數值

邏輯:
1)查找SQL語句中的 #{屬性} ,肯定是第幾個參數,再在方法參數中找到對應的值,存儲下來,替換 #{屬性} 爲? 。
2)查找SQL語句中的 ${屬性} ,肯定是哪一個參數,再在方法參數中找到對應的值,替換 ${屬性} 。
3)返回最終的SQL與參數數組。

 解析過程涉及的數據:SQL語句、方法的參數定義、方法的參數值

@Insert("insert into t_user(id,name,sex,age) values(#{id},#{name},#
{sex},#{age})")
void addUser(String id,@Param("name")String xname,String sex,int age);
Parameter[] params = method.getParameters();
public Object invoke(Object proxy, Method method, Object[] args)

 

 

說明: 這裏是要肯定SQL中的?N是哪一個參數值。
這裏要分三種狀況: 方法參數是0參數,單個參數、多個參數。
0參數:不須要考慮參數了。
單個參數:SQL中的參數取值參數的屬性或就是參數自己。
多個參數:則須要肯定SQL中的參數值取第幾個參數的值。

 多個參數的狀況,能夠有兩種作法:

方式一: 查找#{屬性},根據Parameter[]中的名稱(註解名、序號)匹配肯定是第幾個參數,再到 Object[] args中取到對應的值。
方式二:先將Parameter[] 和 Object[] args轉爲Map,參數名(註解名、序號)爲key,Object參數值爲值;而後再查找SQL語句中的 #{}${},根據裏面的名稱到map中取對應的值。

哪一種方式更好呢?
咱們來看下二者查找過程的輸出:

 

方式一相較於方式二,看起來複雜的地方是要遍歷Parameter[] 來肯定索引號。

思考一下:這種查找對應關係的事,須要每次調用方法時都作嗎?方法的參數會有不少個嗎?

這個對應關係能夠在掃描解析Mapper接口時作一次便可。在調用Mapper代理對象的方法時,
就能夠直接根據索引號去Object[] args中取參數值了。

方式2則每次調用Mapper代理對象的方法時,都須要建立轉換Map。

並且方式一,單個參數與多個參數咱們能夠一樣處理。

 要在掃描解析Mapper接口時作參數解析咱們就須要定義對應的存儲結構,及修改MappedStatement了

?N--- 參數索引號 的對應關係如何表示?
?N 就是一個數值,並且是一個順序數(只是jdbc中的?是從1開始)。咱們徹底能夠用List來存儲。
參數索引號,僅僅是個索引號嗎?

@Insert("insert into t_user(id,name,sex,age,org_id) values(#{user.id},#
{user.name},#{user.sex},#{user.age},#{org.id})")
void addUser(User user,Org org);

 它應該是索引號、和裏面的屬性兩部分。

 

解析階段由它們倆完成這件事:

 

咱們在MappedStatement中再增長一個方法來完成根據參數映射關係獲得真正參數值的方法:

 

把MapperProxy的invoke方法填填看:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
    // TODO 這裏須要完成哪些事?
    // 一、得到方法對應的SQL語句
    String id = this.mapper.getName() + "." + method.getName();
    MappedStatement ms = this.configuration.getMappedStatement(id);
    
    // 二、解析SQL參數與方法參數的對應關係,獲得真正的SQL與語句參數值
    RealSqlAndParamValues rsp = ms.getRealSqlAndParamValues(args);
    
    // 三、得到數據庫鏈接
    Connection conn =
    this.configuration.getDataSource().getConnection();
    
    // 四、執行語句。
    PreparedStatement pst = conn.prepareStatement(rsp.getSql());
    // 疑問:語句必定是PreparedStatement?
    // 設置參數
    if (rsp.getParamValues() != null) {
        int i = 1;
        for (Object p : rsp.getParamValues()) {
            pst.setxxx(i++, p); //這裏寫不下去了.......如何決定該調用pst的哪
            個set方法?
        }
        
    }
    
    // 五、處理結果
    
    return null;
}

4 JavaType、JdbcType轉換

 1 認識它們

JavaType:java中的數據類型。
JdbcType:Jdbc規範中根據數據庫sql數據類型定義的一套數據類型規範,各數據庫廠商遵守這套規範來提供jdbc驅動中數據類型支持。

 

疑問:爲何咱們在這裏須要考慮它呢?
pst.setxxx(i++, p),咱們不能根據p的類型選擇set方法嗎?
看pst的set方法中與對應的:

 

咱們判斷p的類型,而後選擇不可嗎? 像下面這樣

int i = 1;
for (Object p : rsp.getParamValues()) {
    if (p instanceof Byte) {
        pst.setByte(i++, (Byte) p);
    } 
    else if (p instanceof Integer) {
        pst.setInt(i++, (int) p);
    }
    else if (p instanceof String) {
        pst.setString(i++, (String) p);
    }
    ...
    else if(...)
}

咱們來看一下這種狀況:

 

看PreparedStatment的set方法:

 

上面前兩種狀況怎麼處理?
這個須要用戶說明其要使用的JDBCType,否則鬼知道他想要什麼。
讓用戶怎麼指定呢?

#{user.id,jdbcType=TIME}

 

 

javaType有須要指定不呢?
  好像不須要,那就暫放下。

第3種狀況怎麼處理?
這是一個未知的問題,鬼知道未來使用個人框架的人會須要怎麼處理他們的對象呢!
如何以不變應萬變呢?
面向接口編程
定義一個什麼樣的接口呢?
該接口的用途是什麼?

完成Object p 的pst.setXXX()。

2 TypeHandler

 下面這個if-else-if的代碼是否能夠經過TypeHandler,換成策略模式?

int i = 1;
for (Object p : rsp.getParamValues()) {
    if (p instanceof Byte) {
        pst.setByte(i++, (Byte) p);
    } 
    else if (p instanceof Integer) {
        pst.setInt(i++, (int) p);
    }
    else if (p instanceof String) {
        pst.setString(i++, (String) p);
    }
    ...
    else if(...)
}

定義一些經常使用數據類型的TypeHandler.
先不急着去定義,咱們來考慮一下下面的問題。
這個怎麼使用它呢?
在MapperProxy.invoke()中?

int i = 1;
for (Object p : rsp.getParamValues()) {
    TypeHandler th = getTypeHandler(p.getClass);//還須要別的參數嗎?
    th.setParameter(pst,i++,p);
}

 還須要JDBCType。

int i = 1;
for (Object p : rsp.getParamValues()) {
    TypeHandler th = getTypeHandler(p.getClass,jdbcType);//jdbcType 從哪來?
    th.setParameter(pst,i++,p);
}

 

問題:
一、是在invoke方法中來判斷TypeHandler呢?仍是在MappedStatement的getRealSqlAndParamValues時就返回值對應的TypeHandler?

選擇後者更合適!
那SqlAndParamValues中的參數值就不能是Object[]。它是值和TypeHandler兩部分構成。

 

MapperProxy中的代碼就變成下面這樣了:

int i = 1;
for (ParamValue p : rsp.getParamValues()) {
    TypeHandler th = p.getTypeHandler()
    th.setParameter(pst,i++,p.getValue());
}

 二、MappedStatement又從哪裏去獲取TypeHandler呢?

咱們會定義一些經常使用的,用戶可能會提供一些。用戶怎麼提供?存儲到哪裏?
Configuration 吧,它最合適了。以什麼結構來存儲呢?
這裏涉及到查找,須要根據 參數的javaType、jdbcTyp來查找。
那就定義一個map吧,以下這樣如何?

Map<Type,Map<JDBCType,TypeHandler>> typeHandlerMap;

咱們定義一個TypeHandlerRegistry類來持有全部的TypeHandler,Configuration 中則持有TypeHandlerRegistry

 

 同時咱們完善一下TypeHandler

 

三、用戶如何來指定它們的TypeHandler?

在mybatis-config.xml中增長一個元素來讓用戶指定吧。
mybatis-config.dtd

<!ELEMENT configuration (mappers?, typeHandlers?)+ >

<!ELEMENT mappers (mapper*,package*)>

<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>

<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #IMPLIED
type CDATA #IMPLIED
annotation CDATA #IMPLIED
>

<!ELEMENT typeHandlers (typeHandler*,package*)>

<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
class CDATA #REQUIRED
>

既能夠用typeHandler指定單個,也可用package指定掃描的包,掃描包下實現了TypeHandler接口的類

 mybatis-config.xml

<configuration>
    <mappers>
        <mapper resource="com/leesmall/UserMapper.xml"/>
        <mapper url="file:///var/mappers/CourseMapper.xml"/>
        <mapper class="com.study.leesmall.dao.UserDao" />
        <package name="com.study.leesmall.mapper" />
    <mappers>
    <typeHandlers>
        <typeHandler class="com.study.leesmall.type.XoTypeHandler" />
        <package name="com.study.leesmall.type" />
    </typeHandlers>
</configuration>

 解析註冊的工做就交給XMLConfigBuilder

 

四、MappedStatement中來決定TypeHandler,它就須要Configuration

 

 五、可不能夠在解析語句參數關係時,就決定好TypeHandler?

能夠。咱們在ParameterMap中增長typeHandler屬性。

 

用戶在SQL語句參數中必需要指定JDBCType嗎?
  經常使用的數據類型能夠不指定,咱們能夠提供默認的TypeHandler。

public class StringTypeHandler implements TypeHandler {
    @Override
    public Type getType() {
        return String.class;
    }
    @Override
    public JDBCType getJDBCType() {
        return JDBCType.VARCHAR;
    }
    @Override
    public void setParameter(PreparedStatement pst, int index, Object
    paramValue) throws SQLException {
        pst.setString(index, (String) paramValue);
    }
}

用戶在SQL中參數定義沒有指定JDBCType,則咱們能夠直接使用咱們默認的TypeHandler
如 #{user.name}
咱們判斷它的參數類型爲String,就能夠指定它的TypeHandler爲 StringTypeHandler。可能它的數據庫類型不爲VACHAR,而是一個CHAR定長字符,不要緊!由於pst.setString對VARCHAR、CHAR是通用的。

5 執行結果處理

 5.1 執行結果處理要乾的是什麼事

pst.executeUpate()的返回結果是int,影響的行數。
pst.executeQuery()的返回結果是ResultSet。
在獲得SQL語句執行的結果後,要轉爲方法的返回結果進行返回。這就是執行結果處理要乾的事
根據方法的返回值類型來進行相應的處理。
這裏咱們根據SQL語句執行結果的不一樣,分開處理:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
        // TODO 這裏須要完成哪些事?
        // 一、得到方法對應的SQL語句
        String id = this.mapper.getName() + "." + method.getName();
        MappedStatement ms = this.configuration.getMappedStatement(id);
        
        // 二、解析SQL參數與方法參數的對應關係,獲得真正的SQL與語句參數值
        RealSqlAndParamValues rsp = ms.getRealSqlAndParamValues(args);
        
        // 三、得到數據庫鏈接
        Connection conn =
        this.configuration.getDataSource().getConnection();
        
        // 四、建立語句對象。
        PreparedStatement pst = conn.prepareStatement(rsp.getSql());
        
        // 五、設置語句參數
        int i = 1;
        for (ParamValue p : rsp.getParamValues()) {
            TypeHandler th = p.getTypeHandler()
            th.setParameter(pst,i++,p.getValue());
        }
        
        // 六、執行語句並處理結果
        switch (ms.getSqlCommandType()) {
            case INSERT:
            case UPDATE:
            case DELETE:
                int rows = pst.executeUpdate();
                return handleUpdateReturn(rows, ms, method);
            case SELECT:
                ResultSet rs = pst.executeQuery();
                return handleResultSetReturn(rs, ms, method);
        }
}

private Object handleUpdateReturn(int rows, MappedStatement ms, Method
method) {
    // TODO Auto-generated method stub
    return null;
}

private Object handleResultSetReturn(ResultSet rs, MappedStatement ms,
Method method) {
    // TODO Auto-generated method stub
    return null;
}

5.2 pst.executeUpate()的返回結果處理

pst.executeUpate()的返回結果是int
方法的返回值能夠是什麼?
void、int、long 、 其餘的不能夠!

private Object handleUpdateReturn(int rows, MappedStatement ms, Method
method) {
    Class<?> returnType = method.getReturnType();
    if (returnType == Void.TYPE) {
        return null;
    } 
    else if (returnType == int.class || returnType == Integer.class)
    {
        return rows;
    } 
    else if (returnType == long.class || returnType == Long.class) {
        return (long) rows;
    }
    throw new IllegalArgumentException("update類方法的返回值只能是:void/int/Integer/long/Long");
}

5.3 pst.executeQuery()的返回結果處理

pst.executeQuery()的返回結果是ResultSet
方法的返回值能夠是什麼?
  能夠是void、單個值、集合。
單個值能夠是什麼類型的值?
  任意值、(map)

@Select("select count(1) from t_user where sex = #{sex}")
int query(String sex);
@Select(
"select id,name,sex,age,address from t_user where id = #{id}") User queryUser(String id);
@Select(
"select id,name,sex,age,address from t_user where id = #{id}") Map queryUser1(String id);

集合能夠是什麼類型?
  List、Set、數組、Vector

@Select("select id,name,sex,age,address from t_user where sex = #{sex}
order by #{orderColumn}")
List<User> query(String sex, String orderColumn);

@Select("select id,name,sex,age,address from t_user where sex = #{sex}
order by #{orderColumn}")
List<Map> query1(String sex, String orderColumn);

集合的元素能夠是什麼類型的?
  任意類型的,集合只是單個值多作幾遍。
結果集中的列如何與結果、結果的屬性對應?
  根據結果集列名與屬性名對應
若是屬性名與列名不同呢?
  則需用戶顯式說明映射規則。
須要考慮JDBCType --- JavaType的處理嗎?

不管結果是什麼類型的,在這裏咱們都是要完成一件事:從查詢結果中得到數據返回,只是返回類型不一樣,有不一樣的獲取數據的方式。

請思考:如何讓下面這個方法的代碼的寫好後再也不改變?

private Object handleResultSetReturn(ResultSet rs, MappedStatement ms, Object[] args) {
    // TODO Auto-generated method stub
    return null;
}

我須要在此作個抽象,應用策略模式,不一樣的處理實現這個抽象接口。

那麼在handleResultSetReturn()方法中咱們從哪獲得ResultHandler呢?

從MappedStatement 中獲取,每一個語句對象(查詢類型的)中都持有它對應的結果處理器。
在解析準備MappedStatement對象時根據方法的返回值類型選定對應的ResultHandler。

 在handleResultSetReturn方法中只需調用ms中的ResultHandler:

private Object handleResultSetReturn(ResultSet rs, MappedStatement ms, Object[] args) throws Throwable {
    return ms.getResultHandler().handle(rs, args);
}

5.3.1 方法返回單個值

@Select("select count(1) from t_user where sex = #{sex}")
int query(String sex);

@Select("select id,name,sex,age,address from t_user where id = #{id}")
User queryUser(String id);

@Select("select id,name,sex,age,address from t_user where id = #{id}")
Map queryUser1(String id);

一、基本數據類型、String 如何處理?

針對這種狀況,提供對應的ResultHandler實現:

SimpleTypeResultHandler中須要定義什麼屬性?
handle方法中的邏輯該是怎樣的?

public Object handle(ResultSet rs, Object[] args) throws Throwable {
    //從rs中取對應值
    return rs.getXXX(OOO);
}

問題1:該調用rs的哪一個get方法?
得根據返回值來,返回值類型從哪來?
從SimpleTypeResultHandler中取,在建立MappedStatement時,根據反射得到的返回值類型給入到SimpleTypeResultHandler中。

 

SimpleTypeResultHandler的handle方法中的代碼邏輯以下:

private Object  handle(ResultSet rs, Object[] args) throws Throwable {
    Class<?> returnType = method.getReturnType();
    if (returnType == short.class || returnType == Short.class) 
    {
        return rs.getShort(xxx);
    } 
    else if (returnType == int.class || returnType == Integer.class)
    {
        return rs.getInt(xxx);
    } 
    else if (returnType == long.class || returnType == Long.class) 
    {
        return rs.getLong(xxx);
    }
    ...
    return null;

問題2:該取結果集中的哪一列?
若是結果集中只有一列:那就取第1列。
若是結果集中是有多列呢?

問題:結果集中應不該該有多列?
兩種方案:
  一、該返回值狀況下不容許結果集多列。
  二、不限制,用戶指定列名。

 

問題3:這麼多if else 合適嗎?

    不合適,咋辦?策略模式
         
    該定義怎樣的策略?
這是要作什麼事情?
  從結果集中獲取值,跟pst.setXXX同樣。
  可不能夠在TypeHandler中加方法?

public interface TypeHandler<T> {
    Type getType();
    
    JDBCType getJDBCType();
    
    void setParameter(PreparedStatement pst, int index, Object paramValue) throws SQLException;
    
    T getResult(ResultSet rs, String columnName) throws SQLException;
    
    T getResult(ResultSet rs, int columnIndex) throws SQLException;
}
public class StringTypeHandler implements TypeHandler<String> {

    @Override
    public Type getType() {
    return String.class;
    }
    
    @Override
    public JDBCType getJDBCType() {
    return JDBCType.VARCHAR;
    }
    
    @Override
    public void setParameter(PreparedStatement pst, int index, Object
    paramValue) throws SQLException {
        pst.setString(index, (String) paramValue);
    }
    
    @Override
    public String getResult(ResultSet rs, String columnName) throws
    SQLException {
        return rs.getString(columnName);
    }
    
    @Override
    public String getResult(ResultSet rs, int columnIndex) throws
    SQLException {
        return rs.getString(columnIndex);
    }
}

 同樣的,在啓動解析階段完成結果的TypeHandler選定。

 

根據返回值類型,從TypeHandlerRegistry中取,要取,還得有JDBCType,用戶能夠指定,也可不指定,不指定則使用默認的該類型的TypeHandler。

默認TypeHandler如何註冊,修改registerTypeHandler方法的定義:

registerTypeHandler(TypeHandler th,boolean defalut){
    Map<JDBCType,TypeHandler> cmap = typeHandlerMap.get(th.getType);
    
    if(cmap == null)
    {
        cmap = new HashMap<JDBCType,TypeHandler>();
        typeHandlerMap.put(th.getType,cmap);
    }
    camp.put(th.getJDBCType(),th);
    
    if(default)
    {
        cmap.put(DefaultJDBCType.class/null,th);
    }
}

很好,那就能夠在SimpleTypeResultHandler中持有對應的TypeHandler。
問:在SimpleTypeResultHandler中還有必要持有Class<?> returnType嗎?
  不須要,在TypeHandler中有了。

 

SimpleTypeResultHandler 的handle方法代碼就簡單了:

public Object handle(ResultSet rs, Object[] args) throws Throwable {
    if (StringUtils.isNotEmpty(columnName)) {
        return typeHandler.getResult(rs, columnName);
    } 
    else {
        return typeHandler.getResult(rs, columnIndex);
    }
}

2 對象類型返回結果的處理

@Select("select id,name,sex,age,address from t_user where id = #{id}")
User queryUser(String id);

分析:
一、要完成的事情是什麼?
建立對象
從結果集中取數據給到對象

問題:

一、如何建立對象?

反射調用構造方法。
構造方法有多種狀況:

1 未顯式定義構造方法

public class User {
    private String id;
    private String name;
    private String sex;
    ...
    public String getId() {
    return id;
    }
    public void setId(String id) {
    this.id = id;
    }
    ...
}

這種狀況不須要考慮什麼,直接建立對象!

2 顯式定義了一個構造方法

public class User {
    private String id;
    private String name;
    private String sex;
    ...
    
    public User(String id, String name, String sex) {
        super();
        this.id = id;
        this.name = name;
        this.sex = sex;
    }
    
    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    ...
}

此種狀況下,要建立對象,則須要對應的構造參數值。
問題1:構造參數值從哪來?
  ResultSet

問題2:怎麼知道該從ResultSet中取哪一個列的值,取什麼類型的值?
得定義構造參數與ResultSet中列的對應規則,下面的規則是否能夠?
一、優先採用指定列名的方式:用參數名稱當列名、或用戶爲參數指定列名(參數名與列名不
一致時、取不到參數名時);
二、如不能取得參數名,則按參數順序來取對應順序的列。

問題3:用戶如何來指定列名?

註解、xml配置

public User(@Arg(column="id")String id, @Arg(column="xname")String
name, @Arg(column="sex")String sex) {
    super();
    this.id = id;
    this.name = name;
    this.sex = sex;
}
@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface Arg {
    String name() default "";
String column()
default "";
Class
<?> javaType() default void.class;
JdbcType jdbcType()
default JdbcType.UNDEFINED;
Class
<? extends TypeHandler> typeHandler() default UndefinedTypeHandler.class; }

 

<resultMap id="User" type="com.study.leesmall.mybatis.sample.model.User">
    <constructor>
        <arg name="" column="" JdbcType="" javaType="" typeHandler=""/>
    </constructor>
</resultMap>

mybatis-mapper.dtd 中增長以下定義

<!ELEMENT resultMap (constructor?)>
<!ATTLIST resultMap
id CDATA #REQUIRED
type CDATA #REQUIRED
>

<!ELEMENT constructor (arg*)>

<!ELEMENT arg EMPTY>
<!ATTLIST arg
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
name CDATA #IMPLIED
>

問題4:這些映射信息獲得後如何表示、存儲?
定義一個結果映射實體:ResultMap

 

 注意,在建立ResultMap時,當用戶沒有指定TypeHandler或是UndefinedTypeHandler時,要根據type、jdbcType取對應的typeHandler,沒有則爲null;

問題五、ResultMap 元素怎麼表示?
ResultMap類定義自己就是表示一種java類型與JDBCType類型的映射,基本數據類型與複合類型(類)都是java類型。

擴充一下ResultMap便可:

 

注意:這裏有個使用規則須要注意一下:

 若是ResultMap中有TypeHandler,則該結果直接經過調用TypeHandler來得到。沒有TypeHandler時則看有constructorResultMaps沒,有則根據此取結果集中的值來調用對應的構造方法建立對象。

 3 定義了多個構造方法,怎麼辦?

public class User {
    private String id;
    private String name;
    private String sex;
    ...
    
    public User(String id, String name, String sex) {
        super();
        this.id = id;
        this.name = name;
        this.sex = sex;
    }
    
    public User(String id, String name, String sex, int age) {
        super();
        this.id = id;
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
    
    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    ...
}

用戶指定構造方法,沒有指定時則用默認構造方法(沒有則報錯)。
用戶怎麼指定:
註解 :

@MapConstructor
public User(@Arg("id")String id, @Arg("xname")String name,
@Arg("sex")String sex) {
    super();
    this.id = id;
    this.name = name;
    this.sex = sex;
}
/**
* 標識選用的構造方法
*/
@Documented
@Retention(RUNTIME)
@Target(CONSTRUCTOR)
public @interface MapConstructor {

}

xml:根據constructor元素中 arg元素的數量、javaType來肯定構造函數。注意arg有順序規則、必須指定構造方法的所有參數。

<resultMap id="User" type="com.study.leesmall.mybatis.sample.model.User">
    <constructor>
        <arg column="id" javaType="String"/>
        <arg column="name" javaType="String"/>
        <arg column="sex" javaType="String"/>
    </constructor>
</resultMap>

二、該給對象哪些屬性值?

建立出對象後,能夠從結果集中取值來填充對象的屬性。
問題1:該給哪些屬性賦值?
能夠有兩種規則:
一、用戶指定要給哪些屬性賦值。
二、自動映射賦值:取列的值賦給同名的屬性。

二者能夠一塊兒使用。
那麼這裏就涉及兩個事情:
一、用戶如何指定?
註解方式: 咱們給定義一個註解 @Result

public class User {
    @Result
    private String id;
    @Result(column="xname")
    private String name;
    ...
}
@Documented
@Retention(RUNTIME)
@Target({ TYPE, FIELD })
public @interface Result {
    String column() default "";
    
    Class<?> javaType() default void.class;
    
    JdbcType jdbcType() default JdbcType.UNDEFINED;
    
    Class<? extends TypeHandler> typeHandler() default
    UndefinedTypeHandler.class;
}

xml方式:

<resultMap id="User" type="com.study.leesmall.mybatis.sample.model.User">
    <constructor>
        <arg column="id" javaType="String"/>
        <arg column="name" javaType="String"/>
        <arg column="sex" javaType="String"/>
    </constructor>
    
    <result property="age" column="age" />
</resultMap>

mybatis-mapper.dtd

<!ELEMENT resultMap (constructor?,result*)>
<!ATTLIST resultMap
id CDATA #REQUIRED
type CDATA #REQUIRED
>

<!ELEMENT constructor (arg*)>

<!ELEMENT arg EMPTY>
<!ATTLIST arg
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
name CDATA #IMPLIED
>

<!ELEMENT result EMPTY>
<!ATTLIST result
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>

問題:這些信息如何表示、存儲?

二、是否自動映射如何指定?
增長一個屬性便可:

autoMapping="true"

<resultMap id="User" type="com.study.leesmall.mybatis.sample.model.User" autoMapping="true">
    <result property="age" column="age" />
</resultMap>
<!ELEMENT resultMap (constructor?,result*)>
<!ATTLIST resultMap
id CDATA #REQUIRED
type CDATA #REQUIRED
autoMapping (true|false) #IMPLIED
>

註解方式:

/**
標識類對象要進行自動映射
*/
@Documented
@Retention(RUNTIME)
@Target({ TYPE, FIELD })
public @interface AutoMapping {

}
@AutoMapping
public class User {
    @Result
    private String id;
    
    @Result(column="xname")
    private String name;
    ...
}

 

爲方便統一開啓自動映射,咱們能夠在Configuration中設計一個全局配置參數,具體的能夠覆蓋全局的。

 

在哪可配置它?

 在mybatis-config.xml中增長一個配置項便可。

<configuration>
    <settings>
        <setting name="autoMappingBehavior" value="PARTIAL"/>
    </settings>
</configuration>

三、對象中包含對象該如何映射及處理

對象中包對象是個問題,先把問題搞清楚,看下面的語句示例:

<!-- Very Complex Statement -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
        select
            B.id as blog_id,
            B.title as blog_title,
            B.author_id as blog_author_id,
            A.id as author_id,
            A.username as author_username,
            A.password as author_password,
            A.email as author_email,
            A.bio as author_bio,
            A.favourite_section as author_favourite_section,
            P.id as post_id,
            P.blog_id as post_blog_id,
            P.author_id as post_author_id,
            P.created_on as post_created_on,
            P.section as post_section,
            P.subject as post_subject,
            P.draft as draft,
            P.body as post_body
        from Blog B
            left outer join Author A on B.author_id = A.id
            left outer join Post P on B.id = P.blog_id
        where B.id = #{id}
</select>

 再看類

public class Blog {
    private String id;
    
    private String title;
    
    private Author author;
    
    private List<Post> posts;
    ....
}


public class Author {
    private String id;
    
    private String username;
    ...
}


public class Post {
    private String id;
    
    private String subject;
    ...
}

這就是對象中包含對象,要從查詢結果中獲得Blog,Blog的Author posts數據。
這就是ORM中的關係映射問題。
結果的映射是簡單的,由於就是指定裏面的屬性取哪一個列的值。

public class Blog {
    @Result(column="blog_id")
    private String id;
    
    @Result(column="blog_title")
    private String title;
    
    @Result
    private Author author;
    
    @Result
    private List<Post> posts;
    ....
}


public class Author {
    @Result(column="author_id")
    private String id;
    
    @Result(column="author_username")
    private String username;
    ...
}

public class Post {
    @Result(column="post_id")
    private String id;
    
    @Result(column="post_subject")
    private String subject;
    ...
}

 咱們的ResultMap類也是支持的:

可是從結果集中取值來填裝對象則是複雜的!
請先看查詢的結果示例:

 

while(rs.next()){

}

複雜點:不是一行一個Blog對象,處理行時要判斷該行的blog是否已取過了。
問題核心點在哪?
當我操做一行,如何判斷該行的Blog已經取過沒?
這就要求要知道區分Blog的惟一標識、區分Author的惟一標識。怎麼知道?
用戶得告訴咱們他們的id屬性是哪一個,對應的列是哪一個。

讓用戶怎麼來指定id屬性呢?
註解方式:在@Arg 、@Rersult註解中增長id指定項。

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface Arg {
    boolean id() default false;
    
    String name() default "";
    
    String column() default "";
    
    Class<?> javaType() default void.class;
    
    JdbcType jdbcType() default JdbcType.UNDEFINED;
    
    Class<? extends TypeHandler> typeHandler() default
    UndefinedTypeHandler.class;
}
@Documented
@Retention(RUNTIME)
@Target({ TYPE, FIELD })
public @interface Result {
    boolean id() default false;
    
    String column() default "";
    
    Class<?> javaType() default void.class;
    
    JdbcType jdbcType() default JdbcType.UNDEFINED;
    
    Class<? extends TypeHandler> typeHandler() default
    UndefinedTypeHandler.class;
}

xml方式增長:增長argId、id元素

<resultMap id="detailedBlogResultMap" type="Blog">
    <constructor>
        <idArg column="blog_id" javaType="int"/>
    </constructor>
    ....
</resultMap>


<resultMap id="AuthorMap" type="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
</resultMap>

 在ResultMap中增長ID信息

 

問題:要體現出一對一,一對多關係嗎?咱們會在哪裏須要知道這個關係?

 看一個mybatis中的複雜xml ResultMap示例:

<!-- 超複雜的 Result Map -->
<resultMap id="detailedBlogResultMap" type="Blog">
    <constructor>
    <idArg column="blog_id" javaType="int"/>
    </constructor>
    
    <result property="title" column="blog_title"/>
    
    <association property="author" javaType="Author">
        <id property="id" column="author_id"/>
        <result property="username" column="author_username"/>
        <result property="password" column="author_password"/>
        <result property="email" column="author_email"/>
        <result property="bio" column="author_bio"/>
        <result property="favouriteSection" column="author_favourite_section"/>
    </association>
    
    <collection property="posts" ofType="Post">
        <id property="id" column="post_id"/>
        <result property="subject" column="post_subject"/>
        <association property="author" javaType="Author"/>
        <collection property="comments" ofType="Comment">
            <id property="id" column="comment_id"/>
        </collection>
    </collection>
</resultMap>

知道惟一標識了,要判斷前面是否取過了,則還須要有個上下文持有取到的對象,並能根據id列值取到對應的對象。

爲對象類型返回結果定義一個ResultHandler實現ClassTypeResultHandler:

3 Map

@Select("select id,name,sex,age,address from t_user where id = #{id}")
Map queryUser1(String id);

不能在解析階段得到ResultMap
當執行完第一次查詢就能夠肯定下來

 

咱們從結果集中能獲得的是JDBCType

 

問題:

一、key 用什麼?
  用列名
二、取成什麼java類型的值?
  JDBCType中根據整型類型值得到對應的JDBCType

/**
* Returns the {@code JDBCType} that corresponds to the specified
* {@code Types} value
* @param type {@code Types} value
* @return The {@code JDBCType} constant
* @throws IllegalArgumentException if this enum type has no
constant with
* the specified {@code Types} value
* @see Types
*/
public static JDBCType valueOf(int type) {
    for( JDBCType sqlType : JDBCType.class.getEnumConstants()) {
        if(type == sqlType.type)
        return sqlType;
    }
    
    throw new IllegalArgumentException("Type:" + type + " is not a
    valid "+ "Types.java value.");
}

TypeHandler ---> javaType
在TypehandlerRegistry中定義一個JDBCType類型對應的默認的TypeHandler集合,來完成取java值放入到Map中

 

第一次處理結果時,要把這個ResultMaps填充好,後需查詢結果的處理就是直接使用resultMaps

 5.3.2 方法返回集合

返回集合就是單個的重複

if(method.getReturnType() == List.class) {
    Type genericType = method.getGenericReturnType();
    if(genericType == null) {
        // 當集合中放Map
    }
    else if (genericType instanceof ParameterizedType) {
        ParameterizedType t = (ParameterizedType) genericType;
        Class<?> elementType = (Class<?>)t.getActualTypeArguments()[0];
    }
}

----------

相關文章
相關標籤/搜索