`

Mybatis增强型注解简化SQL语句

 
阅读更多

1. 背景

MyBatis提供了简单的Java注解,使得我们可以不配置XML格式的Mapper文件,也能方便的编写简单的数据库操作代码:

public interface UserMapper {
  @Select("SELECT * FROM users WHERE id = #{userId}")
  User getUser(@Param("userId") String userId);
}
  • 1
  • 2
  • 3
  • 4

但是注解对动态SQL的支持一直差强人意,即使MyBatis提供了InsertProvider等*Provider注解来支持注解的Dynamic SQL,也没有降低SQL的编写难度,甚至比XML格式的SQL语句更难编写和维护。
注解的优势在于能清晰明了的看见接口所使用的SQL语句,抛弃了繁琐的XML编程方式。但没有良好的动态SQL支持,往往就会导致所编写的DAO层中的接口冗余,所编写的SQL语句很长,易读性差……
Mybatis在3.2版本之后,提供了LanguageDriver接口,我们可以使用该接口自定义SQL的解析方式。故在这里向大家介绍下以此来实现注解方式下的动态SQL。

2. 实现方案

我们先来看下LanguageDriver接口中的3个方法:

public interface LanguageDriver {
    ParameterHandler createParameterHandler(MappedStatement var1, Object var2, BoundSql var3);

    SqlSource createSqlSource(Configuration var1, XNode var2, Class<?> var3);

    SqlSource createSqlSource(Configuration var1, String var2, Class<?> var3);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. createParameterHandler方法为创建一个ParameterHandler对象,用于将实际参数赋值到JDBC语句中
  2. 将XML中读入的语句解析并返回一个sqlSource对象
  3. 将注解中读入的语句解析并返回一个sqlSource对象

一旦实现了LanguageDriver,我们即可指定该实现类作为SQL的解析器,在XML中我们可以使用 lang 属性来进行指定

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

也可以通过设置指定解析器的方式:

<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>
  • 1
  • 2
  • 3

如果不使用XML Mapper的形式,我们可以使用@Lang注解

public interface Mapper {
  @Lang(MyLanguageDriver.class) 
  @Select("SELECT * FROM users")
  List<User> selectUser();
}
  • 1
  • 2
  • 3
  • 4
  • 5

LanguageDriver的默认实现类为XMLLanguageDriver和RawLanguageDriver;分别为XML和Raw,Mybatis默认是XML语言,所以我们来看看XMLLanguageDriver中是怎么实现的:

public class XMLLanguageDriver implements LanguageDriver {
    public XMLLanguageDriver() {
    }

    public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
    }

    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        if(script.startsWith("<script>")) {
            XPathParser textSqlNode1 = new XPathParser(script, false, configuration.getVariables(),
                                       new XMLMapperEntityResolver());
            return this.createSqlSource(configuration, textSqlNode1.evalNode("/script"), parameterType);
        } else {
            script = PropertyParser.parse(script, configuration.getVariables());
            TextSqlNode textSqlNode = new TextSqlNode(script);
            return (SqlSource)(textSqlNode.isDynamic()?new DynamicSqlSource(configuration, textSqlNode)
                   :new RawSqlSource(configuration, script, parameterType));
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

发现其实mybatis已经帮忙写好了解析逻辑,而且发现如果是以开头的字符串传入后,会被以XML的格式进行解析。那么方案就可以确认了,我们继承XMLLanguageDriver这个类,并且重写其createSqlSource方法,按照自己编写逻辑解析好sql后,再调用父类的方法即可。

3. 实现自定义注解

本段中给出一些常见的自定义注解的实现和使用方式。

3.1 自定义Select In注解

在使用Mybatis注解的时候,发现其对Select In格式的查询支持非常不友好,在字符串中输入十分繁琐,可以通过将自定义的标签转成格式;下面便通过我们自己实现的LanguageDriver来实现SQL的动态解析:

DAO接口层中代码如下:

@Select("SELECT * FROM users WHERE id IN (#{userIdList})")
@Lang(SimpleSelectInLangDriver.class)
List<User> selectUsersByUserId(List<Integer> userIdList);
  • 1
  • 2
  • 3

LanguageDriver实现类如下:

// 一次编写即可
public class SimpleSelectInLangDriver extends XMLLanguageDriver implements LanguageDriver {

    private static final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)");

    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {

        Matcher matcher = inPattern.matcher(script);
        if (matcher.find()) {
            script = matcher.replaceAll("<foreach collection=\"$1\" item=\"_item\" open=\"(\" " +
                                        "separator=\",\" close=\")\" >#{_item}</foreach>");
        }


        script = "<script>" + script + "</script>";
        return super.createSqlSource(configuration, script, parameterType);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

通过自己实现LanguageDriver,在服务器启动的时候,就会将我们自定义的标签解析为动态SQL语句,其等同于:

@Select("SELECT * " +
        "FROM users " +
        "WHERE id IN " +
        "<foreach item='item' index='index' collection='list'open='(' separator=',' close=')'>" +
        "#{item}" +
        "</foreach>")
List<User> selectUsersByUserId(List<Integer> userIdList);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过实现LanguageDriver,剥离了冗长的动态SQL语句,简化了Select In的注解代码。

需要注意的是在使用Select In的时候,请务必在传入的参数前加@Param注解,否则会导致Mybatic找不到参数而抛出异常。

3.2 自定义Update Bean注解

在扩展update注解时,数据库每张表的字段和实体类的字段必须遵循一个约定(数据库中采用下划线命名法,实体类中采用驼峰命名法)。当我们update的时候,会根据每个字段的映射关系,写出如下代码:

<update id="updateUsersById" parameterType="com.lucifer.bean.User">
    UPDATE users
        <set>
        <if test=“userName != null">
            user_name = #{userName} ,
        </if>
        <if test=“password != null">
            password = #{password} ,
        </if>
        <if test=“phone != null">
            phone = #{phone},
        </if>
        <if test=“email != null">
            email = #{email},
        </if>
        <if test=“address != null">
            address = #{address},
        </if>
        <if test="gmtCreated != null">
            gmt_created = #{gmtCreated},
        </if>
        <if test="gmtModified != null">
            gmt_modified = #{gmtModified},
        </if>
    </set>
    WHERE id = #{id}
</update> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

我们可以将实体类中的驼峰式代码转换为下划线式命名方式,这样就可以将这种映射规律自动化
经过实现LanguageDriver后,注解代码为

@Update("UPDATE users (#{user}) WHERE id = #{id}")
@Lang(SimpleUpdateLangDriver.class)
void updateUsersById(User user);
  • 1
  • 2
  • 3

相对于原始的代码量有很大的减少,并且,一个类中字段越多,改善也就越明显。实现方式为:

public class SimpleUpdateLangDriver extends XMLLanguageDriver implements LanguageDriver{

    private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)");

    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        Matcher matcher = inPattern.matcher(script);
        if (matcher.find()) {
            StringBuilder sb = new StringBuilder();
            sb.append("<set>");

            for (Field field : parameterType.getDeclaredFields()) {
                String tmp = "<if test=\"_field != null\">_column=#{_field},</if>";
                sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column",
                        CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())));
            }

            sb.deleteCharAt(sb.lastIndexOf(","));
            sb.append("</set>");

            script = matcher.replaceAll(sb.toString());
            script = "<script>" + script + "</script>";
        }

        return super.createSqlSource(configuration, script, parameterType);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

3.3 自定义Insert Bean注解

同理,我们可以抽象化Insert操作,简化后的Insert注解为

@Insert("INSERT INTO users (#{user})")
@Lang(SimpleInsertLangDriver.class)
void insertUserDAO(User user);
  • 1
  • 2
  • 3

实现方式为:

public class SimpleInsertLangDriver extends XMLLanguageDriver implements LanguageDriver {

    private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)");

    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {

        Matcher matcher = inPattern.matcher(script);
        if (matcher.find()) {
            StringBuilder sb = new StringBuilder();
            StringBuilder tmp = new StringBuilder();
            sb.append("(");

            for (Field field : parameterType.getDeclaredFields()) {
                sb.append(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName()) + ",");
                tmp.append("#{" + field.getName() + "},");
            }

            sb.deleteCharAt(sb.lastIndexOf(","));
            tmp.deleteCharAt(tmp.lastIndexOf(","));
            sb.append(") values (" + tmp.toString() + ")");

            script = matcher.replaceAll(sb.toString());
            script = "<script>" + script + "</script>";
        }

        return super.createSqlSource(configuration, script, parameterType);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

3.4 自定义Select注解

有的业务场景下,我们需要根据对象中的字段进行查询,就会写出如下代码:

<select id="selectUser" resultType="com.lucifer.bean.User">
    SELECT id,user_name,password,phone,address,email
    FROM users
    <where>
        <if test="isDel != null">
            AND is_del = #{isDel}
        </if>
        <if test="userName != null">
            AND user_name = #{userName}
        </if>
        <if test="email != null">
            AND email = #{email}
        </if>
        <if test="phone != null">
            AND phone = #{phone}
        </if>
    </where>
</select>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

和Update操作一样,我们可以实现LanguageDriver将where子句抽象化,以此来简化Select查询语句。简化后代码如下:


@Select("SELECT id,user_name,password,phone,address,email FROM users (#{user})")
@Lang(SimpleSelectLangDriver.class)
void selectUserDAO(User user);
  • 1
  • 2
  • 3
  • 4
public class SimpleSelectLangDriver extends XMLLanguageDriver implements LanguageDriver {

    private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)");

    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {

        Matcher matcher = inPattern.matcher(script);
        if (matcher.find()) {
            StringBuilder sb = new StringBuilder();
            sb.append("<where>");

            for (Field field : parameterType.getDeclaredFields()) {
                String tmp = "<if test=\"_field != null\">AND _column=#{_field}</if>";
                sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column",
                        CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())));
            }

            sb.append("</where>");

            script = matcher.replaceAll(sb.toString());
            script = "<script>" + script + "</script>";
        }

        return super.createSqlSource(configuration, script, parameterType);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

4.排除多余的变量

一个常见的情况是,可能会遇到实体类中的部分字段在数据库中并不存在相应的列,这就需要对多余的不匹配的字段进行逻辑隐藏;我们增加一个自定义的注解,并且对Language的实现稍作修改即可。
注解为:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Invisible {
}
  • 1
  • 2
  • 3
  • 4
public class User {
    ...

    @Invisible
    private List<String> userList;


    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后在实现类中将被该注解声明过的字段排除

for (Field field : parameterType.getDeclaredFields()) {
    if (!field.isAnnotationPresent(Invisible.class)) {  // 排除被Invisble修饰的变量
        String tmp = "<if test=\"_field != null\">_column=#{_field},</if>";
        sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column",
                CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

5.注意事项&遇到的一些坑

  1. 务必确保数据库中列名和实体类中字段能一一对应
  2. 在使用自定义SQL解析器的时候,只能传入一个参数,即相应的对象参数即可;传入多个参数会导致解析器中获得到的class对象改变,使得sql解析异常
  3. Update的实现能满足大部分的业务,但有些业务场景可以会遇到根据查询条件来更新查询参数的情况,比如Update uesrs SET uesr_name = ‘tom’ WHERE user_name = ‘Jack’; 在这中场景的时候请不要使用自定义的SQL解析器
  4. 请使用Mybatis 3.3以上版本。3.2版本有bug,会另开一篇重新描述

6.总结

通过实现Language Driver,我们可以方便的自定义自己的注解。在遵循一些约定的情况下(数据库下划线命名,实体驼峰命名),我们可以大幅度的减少SQL的编写量,并且可以完全的屏蔽掉麻烦的XML编写方式,再也不用编写复杂的动态SQL了有木有

// 简洁的数据库操作

@Select("SELECT * FROM users WHERE id IN (#{userIdList})")
@Lang(SimpleSelectInLangDriver.class)
List<User> selectUsersByUserId(List<Integer> userIdList);


@Insert("INSERT INTO users (#{user})")
@Lang(SimpleInsertLangDriver.class)
void insertUserDAO(User user);


@Update("UPDATE users (#{user}) WHERE id = #{id}")
@Lang(SimpleUpdateLangDriver.class)
void updateUsersById(User user);

@Select("SELECT id,user_name,password,phone,address,email FROM users (#{user})")
@Lang(SimpleSelectLangDriver.class)
void selectUserDAO(User user);
分享到:
评论

相关推荐

    elasticsearch sql支持增删改查且支持mybatis集成

    1. **SQL支持**:Elasticsearch SQL的扩展使得开发者可以使用他们熟悉的SQL语句来操作Elasticsearch索引。这包括了创建、查询、更新和删除数据,使得数据库操作变得更加直观和高效。例如,你可以使用INSERT语句添加...

    ssm整合 - Spring4 + mybatis3 + c3p0(SQL Server).zip

    MyBatis是一个轻量级的持久层框架,它允许开发者将SQL语句直接写在XML或注解中,消除了DAO层的大部分工作。MyBatis 3增强了动态SQL的功能,支持Mapper接口,使得代码更加简洁和易于维护。在SSM整合中,MyBatis作为...

    动态封装SQL语句,配置文件的调用与封装

    在IT行业中,数据库操作是应用开发中的重要环节,而动态封装SQL语句和配置文件的调用与封装是提升程序灵活性、可维护性的重要技术手段。下面将详细讲解这两个概念及其应用。 首先,动态封装SQL语句指的是在运行时...

    SQL语句封装

    - **ORM框架**:如Hibernate、MyBatis等,提供高级封装,将SQL语句与对象映射,通过面向对象的方式操作数据库。 **3. 示例分析** - **Python中的SQLAlchemy**:一个强大的SQL工具包,支持SQL语句的动态执行和编译,...

    MybatisX-1.5.7

    2. **简化SQL编写**:MybatisX提供了动态SQL支持,例如自动生成查询条件、排序等,使得编写复杂的SQL语句更为方便,减少了代码量。 3. **增强型Mapper接口**:在原有Mybatis的基础上,MybatisX添加了一些便捷的方法...

    free-idea-mybatis2020.9.15.rar

    【标签】"mybatis 插件" 指出这个压缩包的重点在于MyBatis的插件,这可能包括增强型的代码提示、自动化配置、SQL调试工具等功能,这些功能能够提升开发者在IDE中编写和管理MyBatis映射器文件、SQL语句的效率。...

    MyBatisPlus 自定义sql语句的实现

    MyBatisPlus 是一个基于 MyBatis 的增强型 ORM 框架,它提供了强大的 CRUD 操作和条件构造器,但是在某些情况下,我们需要自定义 sql 语句来满足业务需求。本文将详细介绍如何在 MyBatisPlus 中自定义 sql 语句,并...

    Spring mvc mybatis plus 实现AOP 切面日志系统

    然后,通过`@Before`、`@After`、`@Around`等注解定义在特定时机执行的增强方法,比如在方法执行前记录开始时间,在方法执行后记录结束时间和执行结果,甚至在方法执行过程中捕获异常并记录。 项目中的"logsServer...

    MyBatis-4.04

    在MyBatis中,你可以直接编写SQL语句,将其配置在Mapper XML文件中,或者使用注解方式定义在接口的方法上,这样既保留了SQL的灵活性,又避免了手动管理JDBC连接的麻烦。 MyBatis的映射文件是其核心组成部分之一,...

    idea2018.1插件 MyBatis Plugins

    MyBatis Plugins是一款专为IntelliJ IDEA设计的增强型插件,它主要针对MyBatis框架进行优化,提供了诸多实用功能,旨在简化MyBatis的日常开发工作。此插件不仅兼容IDEA 2017.3版本,还特别强调对IDEA 2018.1版本的...

    mybatis-3.5.7.zip

    MyBatis是一个强大的Java持久层框架,它简化了与数据库交互的过程,提供了SQL映射功能,使得开发者可以自由地编写SQL语句。MyBatis 3.5.7是该框架的一个更新版本,旨在增强性能、修复已知问题并引入新特性。下面将...

    MyBatis Generator -1.3.3 - 增强版

    虽然MyBatis Generator生成的代码已经很高效,但通过调整生成的SQL语句和优化实体类设计,可以进一步提高查询性能。例如,合理使用`@TableId`和`@TableField`注解,可以实现更精确的字段映射,减少不必要的转换操作...

    mybatis-3.5.2.zip

    在传统的JDBC编程中,我们往往需要手动编写大量的SQL语句和参数绑定,而MyBatis则通过XML或注解方式将SQL与Java代码分离,使得代码更易于维护和测试。 在MyBatis 3.5.2版本中,我们可以看到这个版本继续优化了性能...

    mybatis3.4.5完整jar包下载

    3. **MyBatis-Plus**(可选):这是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。 4. **MyBatis-Generator**:这是一个代码生成器,可以从数据库自动生成相应的Java实体...

    MyBatis包下载(mybatis-3.5.2.rar)

    MyBatis是一个流行的Java持久层框架,它极大地简化了数据库操作,通过将SQL语句与Java代码紧密结合,提供了更高效且灵活的数据库访问方式。在3.5.2这个版本中,MyBatis继续保持着其易用性和强大的功能特性。 首先,...

    mybatis jar包

    9. **MyBatis注解**:除了XML配置外,MyBatis也支持注解方式来编写SQL语句,这使得代码更加简洁,降低了XML配置的工作量。 10. **缓存机制**:MyBatis提供了本地缓存和二级缓存,可以在一定程度上提高性能,减少对...

    黑马程序员燕青Mybatis课堂笔记和源码

    Mybatis 是一款流行的Java持久层框架,它简化了数据库操作,使得开发者能够通过XML或注解方式配置SQL、映射结果集以及处理参数。在这个"黑马程序员燕青Mybatis课堂笔记和源码"资料中,我们可以深入理解Mybatis的核心...

    Free MyBatis plugin 旧版 2019

    "Free MyBatis plugin"是为IntelliJ IDEA开发的一款增强型插件,主要目的是为了提升MyBatis在开发过程中的效率。 这款旧版的"Free MyBatis plugin 2019"可能包含了以下功能和知识点: 1. **智能提示与自动补全**:...

    mybatis官方教程(中文版)

    MyBatis通过使用XML或注解的方式配置SQL语句,并将其与映射的对象进行关联,可以实现对数据库的CRUD(创建、读取、更新、删除)操作。 首先,MyBatis支持自定义SQL、存储过程以及高级映射。这意味着开发者可以根据...

    狂胜说Mybatis.zip

    参数映射让SQL语句中的占位符与Java方法的参数对应,简化了参数处理。 6. **自动类型转换**:Mybatis有内置的类型处理器(TypeHandler),能够自动进行Java类型和JDBC类型之间的转换,例如Date转String,String转...

Global site tag (gtag.js) - Google Analytics