使用Mybatis實現動態SQL

使用Mybatis實現動態SQL

摘要

\quad在知道Mybatis(原名ibatis)怎麼用以前,對於在代碼中鏈接數據庫,我都是用JDBC鏈接的,例如這樣:html

Connection connection = DriverManager.getConnection(jdbcUrl, user, password);
 String sqlCommend = "select goods.id,goods.name,sum(`order`.goods_num*goods_price) as gmv from `order` \n" +
                "join goods\n" +
                "on goods.id = `order`.goods_id\n" +
                "group by goods_id \n" +
                "order by gmv desc\n" +
                "\n";
        try (PreparedStatement pS = databaseConnection.prepareStatement(sqlCommend)) {
            ResultSet resultSet = pS.executeQuery();
            return getGoodsAndGmv(resultSet);
        }
複製代碼

這樣看起來也沒多麻煩,可是誰也不想本身的函數中出現這麼一段不堪的語句。因此,Mybatis爲咱們提供了更加方便的執行數據庫操做的方法。
\quad Mybatis自己也是一種ORM(Object Relationship Mapping)框架,既對象關係映射,說白了就是實現數據庫到Java對象的一個映射,就是咱們與數據庫打交道的一箇中間層。
\quadMybatis的官方文檔寫的很是詳細,你碰到的問題基本上均可以經過官方文檔解決。java

1.從配置開始

\quad 跟着官方文檔一步步走,首先須要從外部引入Mybatis的jar包,使用Maven的話則須要引入Maven配置,接下來就是配置資源文件了。首先明白,在Java中把非代碼的內容都稱爲資源,包括圖片、視頻、數據庫等,資源目錄與代碼目錄結構相似。git

接着在mybatis包下新建config.xml文件,根據文檔提示將xml文件的內容扔進去:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>
複製代碼

注意:github

<!--數據庫的驅動類型-->
        <property name="driver" value="org.h2.Driver"/>
         <!--數據庫的鏈接串-->
        <property name="url" value="jdbc:h2:file:H:/githubitem/SinaCrawler/sina-crawler/SinaCrawler"/>
         <!--用戶名-->
        <property name="username" value="root"/>
         <!--密碼-->
        <property name="password" value="password"/>

複製代碼

以及sql

<mappers>
          <mapper resource="XXX/XXX/XXX/XXX.xml"/>
          <mapper resource="XXX/XXX/XXX/XXX$XXX"/>
         </mappers>
複製代碼

是須要根據本身的實際狀況修改的。這裏用的是本身的一個H2數據庫爲例子,
數據庫中的內容爲:數據庫

用戶表:
+----+----------+------+----------+
| ID | NAME     | TEL  | ADDRESS  |
+----+----------+------+----------+
| 1  | zhangsan | tel1 | beijing  |
+----+----------+------+----------+
| 2  | lisi     | tel2 | shanghai |
+----+----------+------+----------+
| 3  | wangwu   | tel3 | shanghai |
+----+----------+------+----------+
| 4  | zhangsan | tel4 | shenzhen |
+----+----------+------+----------+
商品表:
+----+--------+-------+
| ID | NAME   | PRICE |
+----+--------+-------+
| 1  | goods1 | 10    |
+----+--------+-------+
| 2  | goods2 | 20    |
+----+--------+-------+
| 3  | goods3 | 30    |
+----+--------+-------+
| 4  | goods4 | 40    |
+----+--------+-------+
| 5  | goods5 | 50    |
+----+--------+-------+
訂單表:
+------------+-----------------+------------------+---------------------+-------------------------------+
| ID(訂單ID) | USER_ID(用戶ID) | GOODS_ID(商品ID) | GOODS_NUM(商品數量) | GOODS_PRICE(下單時的商品單價)        |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 1          | 1               | 1                | 5                   | 10                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 2          | 2               | 1                | 1                   | 10                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 3          | 2               | 1                | 2                   | 10                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 4          | 4               | 2                | 4                   | 20                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 5          | 4               | 2                | 100                 | 20                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 6          | 4               | 3                | 1                   | 20                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 7          | 5               | 4                | 1                   | 20                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 8          | 5               | 6                | 1                   | 60                            |
+------------+-----------------+------------------+---------------------+-------------------------------+
複製代碼

接下來先用一個簡單的例子來說解整個過程:
\quad 獲取全部的用戶信息: 寫一個接口:apache

public interface UserMapper{
        @Select("select * from user")
        List<User> getUsers();
    }
複製代碼

實現這個接口:編程

public static void main(String[] args) throws IOException {
        String resource = "db/mybatis/config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //得到SqlSession實例
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        //生成代理類
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            List<User> users = mapper.getUsers();
            for (User user:users
                 ) {
                System.out.println(user);
            }
        }
複製代碼

config.xml配置文件中添加這個接口的絕對路徑安全

<mappers>
         <!-- $區份內部類的分隔符 -->
        <mapper class="com.github.hcsp.sql.Sql$UserMapper"/>
    </mappers>
複製代碼

點擊運行就能夠看到結果了:bash

User{id=1, name='zhangsan', tel='tel1', address='beijing'}
User{id=2, name='lisi', tel='tel2', address='shanghai'}
User{id=3, name='wangwu', tel='tel3', address='shanghai'}
User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}

Process finished with exit code 0
複製代碼

問題來了,咱們並無實現這個接口,那麼結果是怎麼出來的呢?看到MapperRegistry中的getMapper:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
複製代碼

\quad 能夠看到,這裏使用了代理模式,Mybatis識別出了@Select註解,並生成代理類,在代理類中包含接口的實現方法。
再問一個問題:
能夠看到,在咱們在數據庫中查詢數據的結果是這樣的,Mybatis是怎麼把它轉換爲User類的呢?

\quad 其實這也是經過反射完成的,根據每一列的列名去查找User類中的成員變量,根據查到的行數生成對應個數的對象。

2.配置日誌框架

  • 1.根據官方文檔的提示,先在config.xml中添加配置:
<settings>
        <setting name="logImpl" value="LOG4J"/>
     </settings>
複製代碼

注意放置的位置:

要按照這個順序放置配置,因此setting須要放在前面。

  • 2.引入Log4j的maven依賴
  • 3.建立 log4j.properties 文件,複製配置參數到其中:
    注意日誌等級須要修改:
# Global logging configuration
# 日誌等級爲DEBUG,標準輸出
log4j.rootLogger=DEBUG, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
# 標準輸出 = 控制檯輸出源
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
# 標準佈局 = log4j模式化佈局
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# 轉化輸出的模式 = 優先級(佔5個字節)[線程名]
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
複製代碼

再次運行一下試試看:

DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 2076287037.
DEBUG [main] - Setting autocommit to false on JDBC Connection [conn0: url=jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler user=ROOT]
DEBUG [main] - ==>  Preparing: select * from user 
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 4
User{id=1, name='zhangsan', tel='tel1', address='beijing'}
User{id=2, name='lisi', tel='tel2', address='shanghai'}
User{id=3, name='wangwu', tel='tel3', address='shanghai'}
User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}
DEBUG [main] - Resetting autocommit to true on JDBC Connection [conn0: url=jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler user=ROOT]
DEBUG [main] - Closing JDBC Connection [conn0: url=jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler user=ROOT]
DEBUG [main] - Returned connection 2076287037 to pool.

Process finished with exit code 0
複製代碼

如今就能夠看到DEBUG等級及如下的日誌信息了。注意:
log4j.properties文件必須直接放在resources目錄下,不然系統會找不到該文件。 順便介紹一下日誌等級:
\quad 在log4j.jar/org/apache/log4j/Level類中能夠看到有關日誌等級的聲明:

  • TRACE-源信息

\quad TRACE對程序運行沒有影響,既不打印到控制檯也不輸出到文件,主要用以線上調試,若是須要查看TRACE等級的日誌,須要經過elog命令開啓TRACE,或者將程序日誌輸出級別降至TRACE。

  • DEBUG-調試信息

\quad 默認狀況下,打印至終端,可是不歸檔到日誌文件。所以通常用於程序啓動時,查看日誌流水信息。

  • INFO-應用程序運行信息

\quad INFO等級的日誌信息都是一過性的,不會大量反覆輸出。該級別日誌默認狀況下會打印到終端和日誌文件。

  • WARN-警告信息

\quad 代表程序處理中可能遇到的錯誤,以及非法數據。該警告是一過性的,可恢復不影響程序進行。

  • ERROR-錯誤信息

\quad 該錯誤發生後程序任然能夠運行,可是極有可能在某種不正常的狀況下運行。

  • FATAL-致命的錯誤信息

\quad 錯誤直接致使程序沒法啓動,須要當即解決。

  • ALL/OFF-開啓全部信息/關閉全部信息

3.Mybatis中的SQL語句核心Mapper(存放SQL關係映射的文件)

Mapper有兩種:

  • Mapper:接口由Mybatis動態代理,例如interface UserMapper。
    優勢:方便。直接寫一個接口,而後加上@Selelct註解便可。
    缺點:只能寫一些簡單的SQL語句,對於複雜的邏輯性SQL語句,就很差實現了(雖然JDK13中支持多行SQL)。
  • Mapper:用XML編寫複雜SQL。
    優勢:能夠方便的使用Mybatis的強大功能。 缺點:SQL與代碼分離。

詳細介紹第二種Mapper。根據官網提示,新建Mapper.xml文件(文件名本身取),在文件中添加如下內容:

<?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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>
複製代碼

namespace-命名空間,本身隨便取個名字。 id -給本身的SQL語句取名。
resultType -返回值類型。
示例:

<mapper namespace="com.github.hcsp.sql.Sql">
    <select id="selectUsers" resultType="Map">
        select id,name,address,tel from User
    </select>
複製代碼

在主函數中:

try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers"));
        }
複製代碼

結果:

DEBUG [main] - <==      Total: 4
[{ADDRESS=beijing, TEL=tel1, ID=1, NAME=zhangsan}, {ADDRESS=shanghai, TEL=tel2, ID=2, NAME=lisi}, {ADDRESS=shanghai, TEL=tel3, ID=3, NAME=wangwu}, {ADDRESS=shenzhen, TEL=tel4, ID=4, NAME=zhangsan}]

複製代碼

4.詳細介紹Mapper的參數及返回值類型

返回值類型:
\quad前面的實例中,咱們的返回值類型爲Map,因此返回的是鍵值對。那麼,咱們咱們還能夠新建一個類來存放結果。 像這樣

public class User {
    Integer id;
    String name;
    String address;
    String tel;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' + ", address='" + address + '\'' + ", tel='" + tel + '\'' +
                '}';
    }
}
複製代碼

返回值類型爲User的全限定類名

<select id="selectUsers" resultType="com.github.hcsp.sql.User">
複製代碼

結果以下:

[User{id=1, name='zhangsan', address='beijing', tel='tel1'}, User{id=2, name='lisi', address='shanghai', tel='tel2'}, User{id=3, name='wangwu', address='shanghai', tel='tel3'}, User{id=4, name='zhangsan', address='shenzhen', tel='tel4'}]
複製代碼

\quad能夠看到返回值就變成一個個的User了,讀寫參數都是遵照JavaBean約定使用getter()和setter()進行的。
\quad注意:可使用類型別名,簡化resultType。假若有幾十個方法的返回值類型都是User類型,每次都去寫全限定類型實在是麻煩,並且包名不能動,不然返回值類型全都要動,因此設置類型別名頗有必要。在config.xml中添加如下內容:

<typeAliases>
        <typeAlias alias="User" type="com.github.hcsp.sql.Sql.User"/>
</typeAliases>
複製代碼

注意添加順序,那麼返回值類型能夠直接寫:

<select id="selectUsers" resultType="User">
複製代碼

其實Map也是全限定類名java.lang.HashMap的簡寫。
傳入參數:
查找id爲1的用戶,能夠看到:

<E> List<E> selectList(String statement, Object parameter);
複製代碼

selectList還有一個帶parameter的多態方法,對於一個參數的SQL語句

select id,name,address,tel from User where id=#{id}
複製代碼

直接往裏面塞一個參數便可:

sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers",1)
複製代碼

若是有多個參數就放一個類進去:

User user = new User();
        user.id=1;
        user.name = "zhangsan";
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers",user));
        }
複製代碼

設置參數還可使用${},兩者區別在於${}只是簡單的替換,而#{}是防注入的替換。
演示一下SQL注入: 以這條語句爲例:

select id,name,address,tel from User where name='${name}' and id=${id}
複製代碼

我傳入這樣一個User:

User user = new User();
        user.name = "'or 1=1--";
複製代碼

結果以下:

DEBUG [main] - ==>  Preparing: select id,name,address,tel from User where name=''or 1=1--' and id= DEBUG [main] - ==> Parameters: DEBUG [main] - <== Total: 4 [User{id=1, name='zhangsan', tel='tel1', address='beijing'}, User{id=2, name='lisi', tel='tel2', address='shanghai'}, User{id=3, name='wangwu', tel='tel3', address='shanghai'}, User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}] 複製代碼

能夠看到,我拿到了數據庫中的全部內容。因此,${}是不安全的傳參數的方法。
固然,除了每次都新建一個User對象這種耗費內存的方法以外,還能夠用Map:

Map<Object,Object> map = new HashMap<>();
        map.put("name","zhangsan");
        map.put("id",1);
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers",map));
        }
複製代碼

5.動態SQL-Mybatis的靈魂

文字部分Mybatis官網上有說明,也有例子,本身寫兩個加深下理解。

  • if條件判斷動態查找
<select id="selectUsers" resultType="User">
        select id,name,address,tel from User where name='${name}'
        <if test="id !=null">
            and id=${id}
        </if>
    </select>
複製代碼

\quad這裏注意,不要把where、and這種語句寫在if判斷外面,不然像select * from user where這種語句是不符合格式要求的,就會報錯。

  • choose,when,otherwise-相似於if-else條件判斷
    這裏有個小坑,舉例以下:
<select id="selectUser" resultType="User">
        select * from User
        <choose>
            <when test="name==zhangsan">
                where name = 'zhangsan'
            </when>
            <otherwise>
                where name = 'lisi'
            </otherwise>
        </choose>
    </select>
複製代碼

\quad第一次map.put("name","lisi");獲得的是select * from User where name = 'lisi',這沒問題,第二次map.put("name","zhangsan");,獲得的仍是select * from User where name = 'lisi'。這就蹊蹺了,其實緣由在於<when test="name==zhangsan">沒有把zhangsan用``包起來,Mybatis誤覺得zhangsan也是變量,等着你去傳值,而後將name傳入的`zhangsan`與null進行比較。全部不管後面參數怎麼傳,sql語句都不會按照你所想的邏輯去執行。同時這也證實了一點,name、zhangsan這種參數的初始值都是null。

  • where、trim、set
    這個沒什麼好講的,直接看官網的例子一看就明白了。

  • foreach-實現批量更新SQL

先來個簡單的:
找出id在某個集合中的User

<select id="selectIdIn" resultType="User">
        SELECT *
        FROM User
        WHERE id in
        <!-- item/index-佔位符,collection-須要從哪一個集合中找出結果-->
        <foreach item="item" index="index" collection="list"
                 open="(" separator="," close=")">
            #{item}
        </foreach>
    </select>
複製代碼
map.put("list",Arrays.asList(1,2,3,5,6));
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectIdIn",map));
        }
複製代碼

結果以下:

DEBUG [main] - ==>  Preparing: SELECT * FROM User WHERE id in ( ? , ? , ? , ? , ? ) 
DEBUG [main] - ==> Parameters: 1(Integer), 2(Integer), 3(Integer), 5(Integer), 6(Integer)
DEBUG [main] - <==      Total: 3
[User{id=1, name='zhangsan', tel='tel1', address='beijing'}, User{id=2, name='lisi', tel='tel2', address='shanghai'}, User{id=3, name='wangwu', tel='tel3', address='shanghai'}]
複製代碼

foreach幫咱們拼接了一個查找id在某個範圍內的sql語句。
批量向表中插入user:
SQL語句以下:

<insert id="batchInsertUsers" parameterType="map">
        insert into User(id,name,tel,address)
        values
        <foreach item="user" collection="users" separator=",">
            (#{user.id},#{user.name},#{user.tel},#{user.address})
        </foreach>
    </insert>
複製代碼

其實這樣看來Mybatis中的foreach與咱們平時寫的foreach語句很相似,user是迭代的項目,users是被迭代的集合,中間須要逗號鏈接。

DEBUG [main] - ==>  Preparing: insert into User(id,name,tel,address) values (?,?,?,?) , (?,?,?,?) 
DEBUG [main] - ==> Parameters: null, abcd(String), tel-abcd-1(String), addr-abcd(String), null, abcd(String), tel-abcd-2(String), addr-abcd(String)
複製代碼

能夠看到Mybatis幫咱們拼出了批量插入數據的語句。順便說一句parameterType不是必要的。

6.如何實現表的鏈接

查詢訂單信息,只查詢用戶名、商品名齊全的訂單,即INNER JOIN方式
能夠看到:

public class Order {
    private Integer id;
    /** 訂單中的用戶 */
    private User user;
    /** 訂單中的商品 */
    private Goods goods;
    /** 訂單中的總成交金額 */
    private BigDecimal totalPrice;
複製代碼

order表中嵌套了usergoods,那麼Order的id能夠直接得到,如何把從order表中獲取到的結果,賦值給User、Goods類,這就須要使用Mybatis裏面的association嵌套了。

<select id="getInnerJoinOrders" resultMap="order">
        select `order`.id as order_id,
        user.name as user_name,
        goods.name as goods_name,
        `order`.goods_num as goods_num,
        goods.price as goods_price,
        `order`.goods_num * `order`.goods_price as total_price
        from `order`
        inner join goods on goods.id = `order`.goods_id
        inner join user on user.id = `order`.user_id
    </select>
    <resultMap id="order" type="Order">
        <result property="id" column="order_id"/>
        <result property="totalPrice" column="total_price"/>
        <association property="user" javaType="User">
            <result property="name" column="user_name"/>
        </association>
        <association property="goods" javaType="Goods">
            <result property="name" column="goods_name"/>
            <result property="price" column="goods_price"/>
        </association>
    </resultMap>
複製代碼

能夠看到,對於這種狀況,咱們不能直接返回一個確切的resultType,而是返回一個結果映射resultMap,也就是在這個例子中,咱們的查詢結果order會被映射爲另外一個對象。

  • 這裏的Order、Goods都已經使用typeAlias修改過別名了。
  • 都是一對一的關係,因此使用的是<association>標籤。

7.參考資料:

相關文章
相關標籤/搜索