使用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.参考资料:

相关文章
相关标签/搜索