Mybatis學習(1)—— SQL映射

1. 是什麼?

MyBatis是一款支持普通SQL查詢、存儲過程和高級映射持久層框架。MyBatis消除了幾乎全部的JDBC代碼、參數的設置和結果集的檢索。MyBatis可使用簡單的XML或註解用於參數配置和原始映射,將接口和Java POJO(普通Java對象)映射成數據庫中的記錄。html

2. SQL映射

Mybatis能夠把Mapper.xml文件直接映射到對應的接口,調用接口方法會自動去Mapper.xml文件中找到對應的標籤,這個功能就是利用java的動態代理在binding包中實現的java

2.1 SQL映射配置文件解析

如下面的配置文件爲例:sql

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xrq.StudentMapper">
    <select id="selectStudentById" parameterType="int" resultType="Student">
        <![CDATA[
            select * from student where studentId = #{id}
        ]]>
    </select>
</mapper>數據庫

使用<![CDATA[ ... ]]>,能夠保證不管如何<![CDATA[ ... ]]>裏面的內容都會被解析成SQL語句。所以,建議每一條SQL語句都使用<![CDATA[ ... ]]>包含起來,這也是一種規避錯誤的作法。緩存

2.1.1 select

2.1.1.1 多條件查詢查一個結果

<select id="selectStudentById" parameterType="int" resultType="Student">
        <![CDATA[
            select * from student where studentId = #{id}
        ]]>
</select>mybatis

resultType只能是一個實體類,參數要和實體類裏面定義的同樣,好比studentId、studentName,MyBatis將會自動查找這些屬性,而後將它們的值傳遞到預處理語句的參數中去。 app

使用參數的時候使用了」#」,另外還有一個符號」$」也能夠引用參數,使用」#」最重要的做用就是防止SQL注入框架

調用該語句的Java代碼的寫法:fetch

public Student selectStudentByIdAndName(int studentId, String studentName)
{
    SqlSession ss = ssf.openSession();
    Student student = null;
    try
    {
        student = ss.selectOne("com.xrq.StudentMapper.selectStudentByIdAndName", 
                new Student(studentId, studentName, 0, null));
    }
    finally
    {
        ss.close();
    }
    return student;
}ui

2.1.1.2 查詢多個結果

<select id="selectAll" parameterType="int" resultType="Student" flushCache="false" useCache="true" timeout="10000" fetchSize="100" statementType="PREPARED" resultSetType="FORWARD_ONLY">
    <![CDATA[
        select * from student where studentId > #{id};
    ]]>
</select>

屬性的做用:

  • id—-用來和namespace惟一肯定一條引用的SQL語句

  • parameterType—-參數類型,若是SQL語句中的動態參數只有一個,這個屬性無關緊要

  • resultType—-結果類型,注意若是返回結果是集合,應該是集合所包含的類型,而不是集合自己

  • flushCache—-將其設置爲true,不管語句何時被調用,都會致使緩存被清空,默認值爲false

  • useCache—-將其設置爲true,將會致使本條語句的結果被緩存,默認值爲true

  • timeout—-這個設置驅動程序等待數據庫返回請求結果,並拋出異常事件的最大等待值,默認這個參數是不設置的(即由驅動自行處理)

  • fetchSize—-這是設置驅動程序每次批量返回結果的行數,默認不設置(即由驅動自行處理)

  • statementType—-STATEMENT、PREPARED或CALLABLE的一種,這會讓MyBatis選擇使用Statement、PreparedStatement或CallableStatement,默認值爲PREPARED。這個相信大多數朋友本身寫JDBC的時候也只用過PreparedStatement

  • resultSetType—-FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE中的一種,默認不設置(即由驅動自行處理)

調用sql語句的Java程序:

public List<Student> selectStudentsById(int studentId)
{
    SqlSession ss = ssf.openSession();
    List<Student> list = null;
    try
    {
        list = ss.selectList("com.xrq.StudentMapper.selectAll", studentId);
    }
    finally
    {
        ss.close();
    }
    return list;
}

2.1.1.3 使用resultMap來接收查詢結果

使用resultType的方式是有前提的,那就是假定列名和Java Bean中的屬性名存在對應關係,若是名稱不對應,可使用resultMap來解決列名不匹配的問題:

<resultMap type="Student" id="studentResultMap">
    <id property="studentId" column="studentId" />
    <result property="studentName" column="studentName" />
    <result property="studentAge" column="studentAge" />
    <result property="studentPhone" column="studentPhone" />
</resultMap>
<select id="selectAll" parameterType="int" resultMap="studentResultMap" flushCache="false" useCache="true"
        timeout="10000" fetchSize="100" statementType="PREPARED" resultSetType="FORWARD_ONLY">
    <![CDATA[
        select * from student where studentId > #{id};
    ]]>
</select>

這樣就能夠了,注意兩點:

一、resultMap定義中主鍵要使用id

二、resultMap和resultType不能夠同時使用

2.1.2 insert

<insert id="insertOneStudent" parameterType="Student">
    <![CDATA[
        insert into student values(null, #{studentName}, #{studentAge}, #{studentPhone});
    ]]>   
</insert>

<insert id="insertOneStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="studentId">
    <![CDATA[
        insert into student(studentName, studentAge, studentPhone) 
            values(#{studentName}, #{studentAge}, #{studentPhone});
    ]]>   
</insert>

useGeneratedKeys表示讓數據庫自動生成主鍵,keyProperty表示生成主鍵的列。

Java調用代碼:

public void insertOneStudent(String studentName, int studentAge, String studentPhone)
{
    SqlSession ss = ssf.openSession();
    try
    {
        ss.insert("com.xrq.StudentMapper.insertOneStudent", 
            new Student(0, studentName, studentAge, studentPhone));
        ss.commit();
    }
    catch (Exception e)
    {
        ss.rollback();
    }
    finally
    {
        ss.close();
    }
}

2.1.3 update和delete

update:

<update id="updateStudentAgeById" parameterType="Student">
    <![CDATA[
        update student set studentAge = #{studentAge} where 
            studentId = #{studentId};
    ]]>   
</update>

delete:

<delete id="deleteStudentById" parameterType="int">
    <![CDATA[
        delete from student where studentId = #{studentId};
    ]]>   
</delete>

update的Java代碼:

public void updateStudentAgeById(int studentId, int studentAge)
{
    SqlSession ss = ssf.openSession();
    try
    {
        ss.update("com.xrq.StudentMapper.updateStudentAgeById",
                new Student(studentId, null, studentAge, null));
        ss.commit();
    }
    catch (Exception e)
    {
        ss.rollback();
    }
    finally
    {
        ss.close();
    }
}

studentId和studentAge必須是這個順序,互換位置就更新不了學生的年齡了。

3 Mybatis SQL映射的三種方式

3.1 配置文件

3.1.1 SQL映射文件 XXXmapper.xml

如上面介紹的配置方式:

<mapper namespace="com.test.dao.EmployeeMapper">
    <select id="getEmpById" resultType="com.test.beans.Employee">
        select id,last_name lastName,email,gender from tb1_emplyee where id = #{id}
    </select>
</mapper>

3.1.2 全局配置文件 config.xml

使用Mapper標籤進行綁定:

<mappers>
        <mapper resource="EmployeeMapper.xml" />
</mappers>

3.2 註解方式

定義一個接口:

public interface EmployeeMapperAnnotation {
    @Select("select id,last_name lastName,email,gender from tb1_emplyee where id = #{id}")
    public Employee getEmpById(Integer id);
}

而後在全局配置文件中進行註冊:

<mappers>
    <mapper class="com.test.dao.EmployeeMapperAnnotation"></mapper>
</mappers>

3.3 批量註冊

<mappers>
        <package name="com.atguigu.mybatis.dao"/>
</mappers>

【注】映射文件名必須和接口同名,而且放在與接口同一目錄下,否則註冊不了。

以上是簡單介紹,具體映射文件的配置見詳解Java的MyBatis框架中SQL語句映射部分的編寫

4. MyBatis SQL映射的原理

4.1 註冊Mapper

Mybatis在初始化時會把獲取到的Mapper接口註冊到MapperRegistry,註冊的時候建立一個Mapper代理工廠,這個工廠經過JDK的代理建立一個執行對象,建立代理須要的InvocationHandler爲MapperProxy

//接口註冊
public class MapperRegistry {
    public <T> void addMapper(Class<T> type) {
        //若是是接口
        if (type.isInterface()) {
            if (hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                //放到map中, value爲建立代理的工廠
                knownMappers.put(type, new MapperProxyFactory<T>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the mapper parser. If the type is already known, it won't try.
                //這裏是解析Mapper接口裏面的註解
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
}

mybatis使用XMLMapperBuilder類的實例來解析mapper配置文件

4.2 獲取接口對象

從knownMappers中根據接口類型取出對應的代理建立工廠,用該工廠建立代理

public class MapperRegistry {
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
                //取出MapperProxyFactory
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null)
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        try {
                        //建立代理
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
}
 
//建立代理的工廠
public class MapperProxyFactory<T> {
    /**
     * 須要建立代理的接口
     */
    private final Class<T> mapperInterface;
    /**
     * 執行方法的緩存,不須要每次都建立MapperMethod
     */
    private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
 
    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
 
    public Class<T> getMapperInterface() {
        return mapperInterface;
    }
 
    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }
    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        //建立代理, InvocationHanderl是MapperProxy
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface },mapperProxy);
    }
    /**
     * 傳人sqlSession建立代理
     * @param sqlSession
     * @return
     */
    public T newInstance(SqlSession sqlSession) {
        //把代理執行須要用到的對象傳入
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
}

4.3 調用接口方法

調用代理方法會進入到MapperProxy的public Object invoke(Object proxy, Method method, Object[] args)方法

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
 
    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }
 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //若是方法是Object裏面的則直接調用方法
        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
        //獲取執行方法的封裝對象
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //裏面就是找到對應的sql 執行sql語句
        return mapperMethod.execute(sqlSession, args);
    }
    //緩存, 不須要每次都建立
    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            //傳人配置參數
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}

最終執行sql會進入到MapperMethod中execute方法:

//具體的根據接口找到配置文件標籤的類
public class MapperMethod {
 
    private final SqlCommand command;
    private final MethodSignature method;
 
    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        //SqlCommand封裝該接口方法須要執行sql的相關屬性,如:id(name), 類型
        this.command = new SqlCommand(config, mapperInterface, method);
        //執行方法特性進行封裝,用於構造sql參數,判斷執行sql邏輯走哪條分支
        this.method = new MethodSignature(config, method);
    }
 
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        //先找到對應的執行sql類型, sqlSession會調用不一樣方法
        if (SqlCommandType.INSERT == command.getType()) {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
        } else if (SqlCommandType.UPDATE == command.getType()) {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
        } else if (SqlCommandType.DELETE == command.getType()) {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
        } else if (SqlCommandType.SELECT == command.getType()) {//若是是查詢, 須要對返回作判斷處理
            //根據方法的特性判斷進入哪一個執行分支
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
            } else {
                //只查一條數據
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
            }
        } else {
            throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
            throw new BindingException("Mapper method '" + command.getName()
                    + " attempted to return null from a method with a primitive return type (" + method.getReturnType()
                    + ").");
        }
        return result;
    }
}

4.4 流程圖

mapperProxyBuilder建立流程:

mapperProxy建立流程:

sql語句執行流程:

參考

http://www.cnblogs.com/dongying/p/4142476.html

相關文章
相關標籤/搜索