Mybatis-SQL语句构建器类及日志

SQL语句构建器类

问题

Java程序员面对的最痛苦的事情之一就是在Java代码中嵌入SQL语句。这么来做通常是由于SQL语句需要动态来生成-否则可以将它们放到外部文件或者存储过程中。正如你已经看到的那样,MyBatis在它的XML映射特性中有一个强大的动态SQL生成方案。但有时在Java代码内部创建SQL语句也是必要的。此时,MyBatis有另外一个特性可以帮到你,在减少典型的加号,引号,新行,格式化问题和嵌入条件来处理多余的逗号或 AND 连接词之前。事实上,在Java代码中来动态生成SQL代码就是一场噩梦。例如:        

String sql = "SELECT P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME, "
"P.LAST_NAME,P.CREATED_ON, P.UPDATED_ON " +
"FROM PERSON P, ACCOUNT A " +
"INNER JOIN DEPARTMENT D on D.ID = P.DEPARTMENT_ID " +
"INNER JOIN COMPANY C on D.COMPANY_ID = C.ID " +
"WHERE (P.ID = A.ID AND P.FIRST_NAME like ?) " +
"OR (P.LAST_NAME like ?) " +
"GROUP BY P.ID " +
"HAVING (P.LAST_NAME like ?) " +
"OR (P.FIRST_NAME like ?) " +
"ORDER BY P.ID, P.FULL_NAME";

The Solution

MyBatis 3提供了方便的工具类来帮助解决该问题。使用SQL类,简单地创建一个实例来调用方法生成SQL语句。上面示例中的问题就像重写SQL类那样:      

private String selectPersonSql() {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
    SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
    FROM("PERSON P");
    FROM("ACCOUNT A");
    INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
    INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
    WHERE("P.ID = A.ID");
    WHERE("P.FIRST_NAME like ?");
    OR();
    WHERE("P.LAST_NAME like ?");
    GROUP_BY("P.ID");
    HAVING("P.LAST_NAME like ?");
    OR();
    HAVING("P.FIRST_NAME like ?");
    ORDER_BY("P.ID");
    ORDER_BY("P.FULL_NAME");
  }}.toString();
}

该例中有什么特殊之处?当你仔细看时,那不用担心偶然间重复出现的"AND"关键字,或者在"WHERE"和"AND"之间的选择,抑或什么都不选。该SQL类非常注意"WHERE"应该出现在何处,哪里又应该使用"AND",还有所有的字符串链接。        

SQL类

这里给出一些示例:

// Anonymous inner class
public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

// Builder / Fluent style
public String insertPersonSql() {
  String sql = new SQL()
    .INSERT_INTO("PERSON")
    .VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
    .VALUES("LAST_NAME", "#{lastName}")
    .toString();
  return sql;
}

// With conditionals (note the final parameters, required for the anonymous inner class to access them)
public String selectPersonLike(final String id, final String firstName, final String lastName) {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
    FROM("PERSON P");
    if (id != null) {
      WHERE("P.ID like #{id}");
    }
    if (firstName != null) {
      WHERE("P.FIRST_NAME like #{firstName}");
    }
    if (lastName != null) {
      WHERE("P.LAST_NAME like #{lastName}");
    }
    ORDER_BY("P.LAST_NAME");
  }}.toString();
}

public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

public String insertPersonSql() {
  return new SQL() {{
    INSERT_INTO("PERSON");
    VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
    VALUES("LAST_NAME", "#{lastName}");
  }}.toString();
}

public String updatePersonSql() {
  return new SQL() {{
    UPDATE("PERSON");
    SET("FIRST_NAME = #{firstName}");
    WHERE("ID = #{id}");
  }}.toString();
}
方法描述
  • SELECT(String)                  
  • SELECT(String...)                  
开始或插入到                 SELECT子句。                可以被多次调用,参数也会添加到                 SELECT子句。                参数通常使用逗号分隔的列名和别名列表,但也可以是数据库驱动程序接受的任意类型。              
  • SELECT_DISTINCT(String)                  
  • SELECT_DISTINCT(String...)                  
开始或插入到                 SELECT子句,                也可以插入                 DISTINCT关键字到生成的查询语句中。                可以被多次调用,参数也会添加到                 SELECT子句。                 参数通常使用逗号分隔的列名和别名列表,但也可以是数据库驱动程序接受的任意类型。              
  • FROM(String)                  
  • FROM(String...)                  
开始或插入到                 FROM子句。                可以被多次调用,参数也会添加到                 FROM子句。                参数通常是表名或别名,也可以是数据库驱动程序接受的任意类型。              
  • JOIN(String)                  
  • JOIN(String...)                  
  • INNER_JOIN(String)                  
  • INNER_JOIN(String...)                  
  • LEFT_OUTER_JOIN(String)                  
  • LEFT_OUTER_JOIN(String...)                  
  • RIGHT_OUTER_JOIN(String)                  
  • RIGHT_OUTER_JOIN(String...)                  
基于调用的方法,添加新的合适类型的                 JOIN子句。 参数可以包含由列命和join on条件组合成标准的join。              
  • WHERE(String)                  
  • WHERE(String...)                  
插入新的                 WHERE子句条件,                由AND链接。可以多次被调用,每次都由AND来链接新条件。使用                 OR()                 来分隔OR。              
OR()               使用OR来分隔当前的                 WHERE子句条件。 可以被多次调用,但在一行中多次调用或生成不稳定的SQL。              
AND()               使用AND来分隔当前的                 WHERE子句条件。 可以被多次调用,但在一行中多次调用或生成不稳定的SQL。因为                 WHERE                 和                 HAVING                 二者都会自动链接                 AND, 这是非常罕见的方法,只是为了完整性才被使用。              
  • GROUP_BY(String)                  
  • GROUP_BY(String...)                  
插入新的                 GROUP BY子句元素,由逗号连接。 可以被多次调用,每次都由逗号连接新的条件。              
  • HAVING(String)                  
  • HAVING(String...)                  
插入新的                 HAVING子句条件。                由AND连接。可以被多次调用,每次都由AND来连接新的条件。使用                 OR()                 来分隔OR.              
  • ORDER_BY(String)                  
  • ORDER_BY(String...)                  
插入新的                 ORDER BY子句元素,                由逗号连接。可以多次被调用,每次由逗号连接新的条件。              
DELETE_FROM(String)               开始一个delete语句并指定需要从哪个表删除的表名。通常它后面都会跟着WHERE语句!              
INSERT_INTO(String)               开始一个insert语句并指定需要插入数据的表名。后面都会跟着一个或者多个VALUES() or INTO_COLUMNS() and INTO_VALUES()。              
  • SET(String)                  
  • SET(String...)                  
针对update语句,插入到"set"列表中
UPDATE(String)               开始一个update语句并指定需要更新的表明。后面都会跟着一个或者多个SET(),通常也会有一个WHERE()。               
VALUES(String, String)               插入到insert语句中。第一个参数是要插入的列名,第二个参数则是该列的值。              
INTO_COLUMNS(String...)                               Appends columns phrase to an insert statement.                This should be call INTO_VALUES() with together.              
INTO_VALUES(String...)                               Appends values phrase to an insert statement.                This should be call INTO_COLUMNS() with together.              

Since version 3.4.2, you can use variable-length arguments as follows:

public String selectPersonSql() {
  return new SQL()
    .SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
    .FROM("PERSON P", "ACCOUNT A")
    .INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
    .WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
    .ORDER_BY("P.ID", "P.FULL_NAME")
    .toString();
}

public String insertPersonSql() {
  return new SQL()
    .INSERT_INTO("PERSON")
    .INTO_COLUMNS("ID", "FULL_NAME")
    .INTO_VALUES("#{id}", "#{fullName}")
    .toString();
}

public String updatePersonSql() {
  return new SQL()
    .UPDATE("PERSON")
    .SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
    .WHERE("ID = #{id}")
    .toString();
}

SqlBuilder 和 SelectBuilder (已经废弃)

在3.2版本之前,我们使用了一点不同的做法,通过实现ThreadLocal变量来掩盖一些导致Java DSL麻烦的语言限制。但这种方式已经废弃了,现代的框架都欢迎人们使用构建器类型和匿名内部类的想法。因此,SelectBuilder 和 SqlBuilder 类都被废弃了。        

          下面的方法仅仅适用于废弃的SqlBuilder 和 SelectBuilder 类。        

方法描述
BEGIN()            /             RESET()           这些方法清空SelectBuilder类的ThreadLocal状态,并且准备一个新的构建语句。开始新的语句时,             BEGIN()读取得最好。 由于一些原因(在某些条件下,也许是逻辑需要一个完全不同的语句),在执行中清理语句             RESET()读取得最好。          
SQL()           返回生成的             SQL()             并重置             SelectBuilder             状态 (好像             BEGIN()             或             RESET()             被调用了).             因此,该方法只能被调用一次!          

          SelectBuilder 和 SqlBuilder 类并不神奇,但是知道它们如何工作也是很重要的。           SelectBuilder 使用 SqlBuilder 使用了静态导入和ThreadLocal变量的组合来开启整洁语法,可以很容易地和条件交错。使用它们,静态导入类的方法即可,就像这样(一个或其它,并非两者):        

import static org.apache.ibatis.jdbc.SelectBuilder.*;
import static org.apache.ibatis.jdbc.SqlBuilder.*;

这就允许像下面这样来创建方法:

/* DEPRECATED */
public String selectBlogsSql() {
  BEGIN(); // Clears ThreadLocal variable
  SELECT("*");
  FROM("BLOG");
  return SQL();
}
/* DEPRECATED */
private String selectPersonSql() {
  BEGIN(); // Clears ThreadLocal variable
  SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
  SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
  FROM("PERSON P");
  FROM("ACCOUNT A");
  INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
  INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
  WHERE("P.ID = A.ID");
  WHERE("P.FIRST_NAME like ?");
  OR();
  WHERE("P.LAST_NAME like ?");
  GROUP_BY("P.ID");
  HAVING("P.LAST_NAME like ?");
  OR();
  HAVING("P.FIRST_NAME like ?");
  ORDER_BY("P.ID");
  ORDER_BY("P.FULL_NAME");
  return SQL();
}

日志

 

     

Mybatis 的内置日志工厂提供日志功能,内置日志工厂将日志交给以下其中一种工具作代理:      

  • SLF4J        
  •           Apache Commons Logging        
  •           Log4j 2
  •           Log4j        
  •           JDK logging        
     

MyBatis 内置日志工厂基于运行时自省机制选择合适的日志工具。它会使用第一个查找得到的工具(按上文列举的顺序查找)。如果一个都未找到,日志功能就会被禁用。

     

不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging,所以在这种配置环境下的 MyBatis 会把它作为日志工具,记住这点非常重要。这将意味着,在诸如 WebSphere 的环境中,它提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。MyBatis 将你的 Log4J 配置忽略掉是相当令人郁闷的(事实上,正是因为在这种配置环境下,MyBatis 才会选择使用 Commons Logging 而不是 Log4J)。如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志工具,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择别的日志工具。

<configuration>
  <settings>
    ...
    <setting name="logImpl" value="LOG4J"/>
    ...
  </settings>
</configuration>
     

logImpl 可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了接口 org.apache.ibatis.logging.Log 的,且构造方法是以字符串为参数的类的完全限定名。(译者注:可以参考org.apache.ibatis.logging.slf4j.Slf4jImpl.java的实现)      

     

你也可以调用如下任一方法来使用日志工具:      

org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();
     

如果你决定要调用以上某个方法,请在调用其它 MyBatis 方法之前调用它。另外,仅当运行时类路径中存在该日志工具时,调用与该日志工具对应的方法才会生效,否则 MyBatis 一概忽略。如你环境中并不存在 Log4J,你却调用了相应的方法,MyBatis 就会忽略这一调用,转而以默认的查找顺序查找日志工具。      

     

关于 SLF4J、Apache Commons Logging、Apache Log4J 和 JDK Logging 的 API 介绍不在本文档介绍范围内。不过,下面的例子可以作为一个快速入门。关于这些日志框架的更多信息,可以参考以下链接:

     

日志配置

       

你可以对包、映射类的全限定名、命名空间或全限定语句名开启日志功能来查看 MyBatis 的日志语句。        

       

再次说明下,具体怎么做,由使用的日志工具决定,这里以 Log4J 为例。配置日志功能非常简单:添加一个或多个配置文件(如 log4j.properties),有时需要添加 jar 包(如 log4j.jar)。下面的例子将使用 Log4J 来配置完整的日志服务,共两个步骤:        

 

          步骤 1:添加 Log4J 的 jar 包        

       

因为我们使用的是 Log4J,就要确保它的 jar 包在应用中是可用的。要启用 Log4J,只要将 jar 包添加到应用的类路径中即可。Log4J 的 jar 包可以在上面的链接中下载。        

       

对于 web 应用或企业级应用,则需要将 log4j.jar 添加到 WEB-INF/lib 目录下;对于独立应用,可以将它添加到JVM 的 -classpath 启动参数中。        

 

          步骤 2:配置 Log4J        

       

配置 Log4J 比较简单,假如你需要记录这个映射器接口的日志:         

package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}
       

在应用的类路径中创建一个名称为 log4j.properties 的文件,文件的具体内容如下:        

# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
       

添加以上配置后,Log4J 就会记录 org.mybatis.example.BlogMapper 的详细执行操作,且仅记录应用中其它类的错误信息(若有)。

       

你也可以将日志的记录方式从接口级别切换到语句级别,从而实现更细粒度的控制。如下配置只对 selectBlog 语句记录日志:        

log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
                

与此相对,可以对一组映射器接口记录日志,只要对映射器接口所在的包开启日志功能即可:

log4j.logger.org.mybatis.example=TRACE
                

某些查询可能会返回庞大的结果集,此时只想记录其执行的 SQL 语句而不想记录结果该怎么办?为此,Mybatis 中 SQL 语句的日志级别被设为DEBUG(JDK 日志设为 FINE),结果的日志级别为 TRACE(JDK 日志设为 FINER)。所以,只要将日志级别调整为 DEBUG 即可达到目的:        

log4j.logger.org.mybatis.example=DEBUG
                

要记录日志的是类似下面的映射器文件而不是映射器接口又该怎么做呢?        

<?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>

如需对 XML 文件记录日志,只要对命名空间增加日志记录功能即可:

log4j.logger.org.mybatis.example.BlogMapper=TRACE

要记录具体语句的日志可以这样做:

log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE

你应该注意到了,为映射器接口和 XML 文件添加日志功能的语句毫无差别。

注意 如果你使用的是 SLF4J 或 Log4j 2,MyBatis 将以 MYBATIS 这个值进行调用。

       

配置文件 log4j.properties 的余下内容是针对日志输出源的,这一内容已经超出本文档范围。关于 Log4J 的更多内容,可以参考Log4J 的网站。不过,你也可以简单地做做实验,看看不同的配置会产生怎样的效果。

posted @ 2018-12-06 10:14  Fighting_Chen  阅读(731)  评论(0编辑  收藏  举报