框架源码系列十二: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];
    }
}

----------

相关文章
相关标签/搜索