整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

利用反射跟自定义注解拼接实体对象的查询SQL

利用反射跟自定义注解拼接实体对象的查询SQL

 前言

  项目中虽然有ORM映射框架来帮我们拼写SQL,简化开发过程,降低开发难度。但难免会出现需要自己拼写SQL的情况,这里分享一个利用反射跟自定义注解拼接实体对象的查询SQL的方法。


  代码

  自定义注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Like {

}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Between {

    /**
     * 最小值的实体属性名
     */
    String min();

    /**
     * 最大值的实体属性名
     */
    String max();
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface In {

    /**
     * in的具体集合的属性名
     */
    String values();
}


  实体对象:

@Data
@Entity
@Table(name="RES_LOG")
public class ResLog {
    @Id
    private String logId;
    private String resourceType;
    private String resourceId;
    @Like //开启模糊查询
    private String resourceName;
    private String resourceCode;
    @In(values="operationTypeList")//in查询
    private String operationType;
    @Between(min="operationTimeStart", max="operationTimeEnd")//开启区间查询
    private Date operationTime;
    private String operatorId;
    private String operator;

    @Transient
    private Date operationTimeStart;
    @Transient
    private Date operationTimeEnd;
    @Transient
    private List<String> operationTypeList;

}


  拼接SQL方法:

/**
 * 自动拼接原生SQL的“and”查询条件,支持自定义注解:@Like @Between @In
 *
 * @param entity           实体对象
 * @param sql              待拼接SQL
 * @param ignoreProperties 忽略属性
 */
public static void appendQueryColumns(Object entity, StringBuilder sql, String... ignoreProperties) {

    try {
        //忽略属性
        List<String> ignoreList1=Arrays.asList(ignoreProperties);
        //默认忽略分页参数
        List<String> ignoreList2=Arrays.asList("class", "pageable", "page", "rows", "sidx", "sord");
   
        //反射获取Class的属性(Field表示类中的成员变量)
        for (Field field : entity.getClass().getDeclaredFields()) {
            //获取授权
            field.setAccessible(true);
            //属性名称
            String fieldName=field.getName();
            //属性的值
            Object fieldValue=field.get(entity);
            //检查Transient注解,是否忽略拼接
            if (!field.isAnnotationPresent(Transient.class)) {
                String column=new PropertyNamingStrategy.SnakeCaseStrategy().translate(fieldName).toLowerCase();
                //值是否为空
                if (!StringUtils.isEmpty(fieldValue)) {
                    //映射关系:对象属性(驼峰)->数据库字段(下划线)
                    if (!ignoreList1.contains(fieldName) && !ignoreList2.contains(fieldName)) {
                        //开启模糊查询
                        if (field.isAnnotationPresent(Like.class)) {
                            sql.append(" and " + column + " like '%" + escapeSql(fieldValue) + "%'");
                        }
                        //开启等值查询
                        else {
                            sql.append(" and " + column + "='" + escapeSql(fieldValue) + "'");
                        }
                    }
                } else {
                    //开启区间查询
                    if (field.isAnnotationPresent(Between.class)) {
                        //获取最小值
                        Field minField=entity.getClass().getDeclaredField(field.getAnnotation(Between.class).min());
                        minField.setAccessible(true);
                        Object minVal=minField.get(entity);
                        //获取最大值
                        Field maxField=entity.getClass().getDeclaredField(field.getAnnotation(Between.class).max());
                        maxField.setAccessible(true);
                        Object maxVal=maxField.get(entity);
                        //开启区间查询
                        if (field.getType().getName().equals("java.util.Date")) {
                            if (!StringUtils.isEmpty(minVal)) {
                                sql.append(" and " + column + " > to_date( '" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) minVal) + "','yyyy-mm-dd hh24:mi:ss')");
                            }
                            if (!StringUtils.isEmpty(maxVal)) {
                                sql.append(" and " + column + " < to_date( '" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) maxVal) + "','yyyy-mm-dd hh24:mi:ss')");
                            }
                        }
                    }
                    
                    //开启in查询
                    if (field.isAnnotationPresent(In.class)) {
                        //获取要in的值
                        Field values=entity.getClass().getDeclaredField(field.getAnnotation(In.class).values());
                        values.setAccessible(true);
                        List<String> valuesList=(List<String>) values.get(entity);
                        if (valuesList !=null && valuesList.size() > 0) {
                            String inValues="";
                            for (String value : valuesList) {
                                inValues=inValues + "'" + value + "'";
                            }
                            sql.append(" and " + column + " in (" + escapeSql(inValues) + ")");
                        }
                    }
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}


  2019-10-24补充:注意!我们这属于动态拼写SQL,需要进行转义防范SQL注入!

/**
     * sql转义
     */
    public static String escapeSql(String str) {
        if (str==null) {
            return null;
        }
        StringBuilder sb=new StringBuilder();
        for (int i=0; i < str.length(); i++) {
            char src=str.charAt(i);
            switch (src) {
                case '\'':
                    sb.append("''");// hibernate转义多个单引号必须用两个单引号
                    break;
                case '\"':
                case '\\':
                    sb.append('\\');
                default:
                    sb.append(src);
                    break;
            }
        }
        return sb.toString();
    }


  测试与效果

public static void main(String[] args) {
    ResLog resLog=new ResLog();
    resLog.setLogId("id1");//等值查询
    resLog.setResourceName("name1");//like查询
    resLog.setOperationTimeStart(new Date());//日期区间查询
    resLog.setOperationTimeEnd(new Date());
    ArrayList<String> list=new ArrayList<>();
    list.add("type1");
    list.add("type2");
    resLog.setOperationTypeList(list);//in查询
    //在外面拼写select * from 是为了多表联查时的情况
    StringBuilder sql=new StringBuilder("select * from res_log where '1'='1'");
    appendQueryColumns(resLog,sql);
    System.out.println(sql.toString());
}


  拼接结果:

select *
  from res_log
 where '1'='1'
   and log_id='id1'
   and resource_name like '%name1%'
   and operation_type in ('type1''type2')
   and operation_time >
       to_date('2018-10-08 15:00:40', 'yyyy-mm-dd hh24:mi:ss')
   and operation_time <
       to_date('2018-10-08 15:00:40', 'yyyy-mm-dd hh24:mi:ss')


  后记

  甚至我们可以直接获取实体对象对应的表名,直接在方法里面拼出 select * from ,这样就不需要在外面拼接这一句

//获取实体对象对应的表名
String TableName=entity.getClass().getAnnotation(Table.class).name();
System.out.println(TableName);

  为了优化SQL,一般我们不建议select * from,而是需要查询那些字段就拼出那些字段,例如:select log_id from

  但是如果数据表有一百个字段呢?一个个手动拼接就太傻了,因此写了一个自动拼接字段的方法,支持配置忽略拼接的字段

/**
     *
     * @param entity 实体对象
     * @param ignoreProperties 动态参数  忽略拼接的字段
     * @return sql
     */
    public static StringBuilder appendFields(Object entity, String... ignoreProperties) {
        StringBuilder sql=new StringBuilder();
        List<String> ignoreList=Arrays.asList(ignoreProperties);
        try {
            sql.append("select ");

            for (Field field : entity.getClass().getDeclaredFields()) {
                //获取授权
                field.setAccessible(true);
                String fieldName=field.getName();//属性名称
                Object fieldValue=field.get(entity);//属性的值
                //非临时字段、非忽略字段
                if (!field.isAnnotationPresent(Transient.class) && !ignoreList.contains(fieldName)) {
                    //拼接查询字段  驼峰属性转下划线
                    sql.append(new PropertyNamingStrategy.SnakeCaseStrategy().translate(fieldName).toLowerCase()).append(" ").append(",");
                }
            }
            //处理逗号(删除最后一个字符)
            sql.deleteCharAt(sql.length() - 1);

            String tableName=entity.getClass().getAnnotation(Table.class).name();
            sql.append("from ").append(tableName).append(" where '1'='1' ");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return sql;
    }

  接着上面的main测试

public static void main(String[] args) {
    ResLog resLog=new ResLog();
    resLog.setLogId("id1");//等值查询
    resLog.setResourceName("name1");//like查询
    resLog.setOperationTimeStart(new Date());//日期区间查询
    resLog.setOperationTimeEnd(new Date());
    ArrayList<String> list=new ArrayList<>();
    list.add("type1");
    list.add("type2");
    resLog.setOperationTypeList(list);//in查询
    //动态拼接查询字段
    StringBuilder sql=appendFields(resLog,"remark","operator");
    appendQueryColumns(resLog,sql);
    System.out.println(sql.toString());
}

  结果

select log_id,
       resource_type,
       resource_id,
       resource_name,
       resource_code,
       operation_type,
       operation_time,
       operator_id
  from RES_LOG
 where '1'='1'
   and log_id='id1'
   and resource_name like '%name1%'
   and operation_type in ('type1''type2')
   and operation_time >
       to_date('2018-12-13 10:34:33', 'yyyy-MM-dd hh24:mi:ss')
   and operation_time <
       to_date('2018-12-13 10:34:33', 'yyyy-MM-dd hh24:mi:ss')



  更新

  2020-10-10更新

  在之前的操作中,我们直接在与数据表映射的entity实体类中使用自定义注解,实体类负责与数据表进行映射,具有共有属性,不应该被业务污染,而实体类对应Vo类负责接参、传参等传输数据的责任,不同的业务冲突时可以创建多个Vo类来解决,正合适我们使用自定义注解来拼接SQL,因此,改成在Vo类中使用我们的自定义注解


  首先,SqlUtil类需要进行一些调整,主要是对appendQueryColumns方法的调整,同时新增了一个拼写全部SQL的聚合方法joinSqlByEntityAndVo

package cn.huanzi.qch.baseadmin.util;

import cn.huanzi.qch.baseadmin.annotation.Between;
import cn.huanzi.qch.baseadmin.annotation.In;
import cn.huanzi.qch.baseadmin.annotation.Like;
import cn.huanzi.qch.baseadmin.common.pojo.PageCondition;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.Transient;
import org.springframework.util.StringUtils;

import javax.persistence.Table;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
 * 拼接SQL工具类
 * 详情请阅读博客:https://www.cnblogs.com/huanzi-qch/p/9754846.html
 */
@Slf4j
public class SqlUtil {

    /**
     * 日期转换格式
     */
    private static SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 数据库驱动类,用于判断数据库类型
     * MySQL:com.mysql.cj.jdbc.driver(默认)
     * postgresql:org.postgresql.driver
     * Oracle:oracle.jdbc.oracledriver
     */
    @Value("${string.datasource.driver-class-name:com.mysql.cj.jdbc.Driver}")
    private static String sqlType;

    /**
     * 根据实体、Vo直接拼接全部SQL
     * @param entityClass 实体类
     * @param entityVo    继承了PageCondition分页条件的Vo类
     * @return sql
     */
    public static StringBuilder joinSqlByEntityAndVo(Class<?> entityClass,Object entityVo){
        //select 所有字段 from table
        StringBuilder sql=SqlUtil.appendFields(entityClass);

        //拼接查询字段
        SqlUtil.appendQueryColumns(entityClass,entityVo,sql);

        //拼接排序字段
        SqlUtil.orderByColumn((PageCondition)entityVo,sql);

        return sql;
    }

    /**
     * 自动拼接原生SQL的“and”查询条件,
     * 支持自定义注解,注解改成打在vo类中,不应该破坏公用的entity实体映射类:@Like @Between @In
     *
     * @param entityClass      实体类
     * @param entityVo         继承了PageCondition分页条件的Vo类
     * @param sql              待拼接SQL
     * @param ignoreProperties 忽略属性
     */
    public static void appendQueryColumns(Class<?> entityClass, Object entityVo, StringBuilder sql, String... ignoreProperties) {
        try {

            List<String> ignoreList1=Arrays.asList(ignoreProperties);
            //默认忽略分页参数
            List<String> ignoreList2=Arrays.asList("class", "pageable", "page", "rows", "sidx", "sord");

            //反射获取Class的属性(Field表示类中的成员变量)
            Class<?> entityVoClass=entityVo.getClass();

            //可以直接传进来,也可以根据entityVoClass来创建entityClass,如果选择动态拼接,对命名规则有一定要求
//            Class<?> entityClass=Class.forName(entityVoClass.getName().replaceFirst("Vo",""));

            for (Field field : entityVoClass.getDeclaredFields()) {
                //获取授权
                field.setAccessible(true);
                //属性名称
                String fieldName=field.getName();
                //属性的值
                Object fieldValue=field.get(entityVo);

                //检查entity中是否也存在该字段,如果没有,直接跳过
                try {
                    entityClass.getDeclaredField(fieldName);
                }catch (NoSuchFieldException e){
                    log.debug("entity中没有这个字段,拼接查询SQL直接跳过:" + e.getMessage());
                    continue;
                }

                String column=SqlUtil.translate(fieldName);

                //值是否为空
                if (!StringUtils.isEmpty(fieldValue)) {
                    //映射关系:对象属性(驼峰)->数据库字段(下划线)
                    if (!ignoreList1.contains(fieldName) && !ignoreList2.contains(fieldName)) {
                        //开启模糊查询
                        if (field.isAnnotationPresent(Like.class)) {
                            sql.append(" and ").append(column).append(" like '%").append(SqlUtil.escapeSql((String) fieldValue)).append("%'");
                        }
                        //开启等值查询
                        else {
                            sql.append(" and ").append(column).append("='").append(SqlUtil.escapeSql((String) fieldValue)).append("'");
                        }
                    }
                } else {
                    //开启区间查询
                    if (field.isAnnotationPresent(Between.class)) {
                        //获取最小值
                        Field minField=entityVoClass.getDeclaredField(field.getAnnotation(Between.class).min());
                        minField.setAccessible(true);
                        Object minVal=minField.get(entityVo);
                        //获取最大值
                        Field maxField=entityVoClass.getDeclaredField(field.getAnnotation(Between.class).max());
                        maxField.setAccessible(true);
                        Object maxVal=maxField.get(entityVo);
                        //开启区间查询,需要使用对应的函数
                        if (field.getType().getName().equals("java.util.Date")) {
                            //MySQL
                            if(sqlType.toLowerCase().contains("com.mysql.cj.jdbc.driver")){
                                if (!StringUtils.isEmpty(minVal)) {
                                    sql.append(" and ").append(column).append(" > str_to_date( '").append(simpleDateFormat.format((Date) minVal)).append("','%Y-%m-%d %H:%i:%s')");
                                }
                                if (!StringUtils.isEmpty(maxVal)) {
                                    sql.append(" and ").append(column).append(" < str_to_date( '").append(simpleDateFormat.format((Date) maxVal)).append("','%Y-%m-%d %H:%i:%s')");
                                }
                            }
                            //postgresql
                            if(sqlType.toLowerCase().contains("org.postgresql.driver")){
                                if (!StringUtils.isEmpty(minVal)) {
                                    sql.append(" and ").append(column).append(" > cast('").append(simpleDateFormat.format((Date) minVal)).append("' as timestamp)");
                                }
                                if (!StringUtils.isEmpty(maxVal)) {
                                    sql.append(" and ").append(column).append(" < cast('").append(simpleDateFormat.format((Date) maxVal)).append("' as timestamp)");
                                }
                            }
                            //Oracle
                            if(sqlType.toLowerCase().contains("oracle.jdbc.oracledriver")){
                                if (!StringUtils.isEmpty(minVal)) {
                                    sql.append(" and ").append(column).append(" > to_date( '").append(simpleDateFormat.format((Date) minVal)).append("','yyyy-mm-dd hh24:mi:ss')");
                                }
                                if (!StringUtils.isEmpty(maxVal)) {
                                    sql.append(" and ").append(column).append(" < to_date( '").append(simpleDateFormat.format((Date) maxVal)).append("','yyyy-mm-dd hh24:mi:ss')");
                                }
                            }
                        }
                    }

                    //开启in查询
                    if (field.isAnnotationPresent(In.class)) {
                        //获取要in的值
                        Field values=entityVoClass.getDeclaredField(field.getAnnotation(In.class).values());
                        values.setAccessible(true);
                        List<String> valuesList=(List<String>) values.get(entityVo);
                        if (valuesList !=null && valuesList.size() > 0) {
                            StringBuilder inValues=new StringBuilder();
                            for (int i=0; i < valuesList.size(); i++) {
                                inValues.append("'").append(SqlUtil.escapeSql(valuesList.get(i))).append("'");
                                if(i < valuesList.size()-1){
                                    inValues.append(",");
                                }
                            }

                            sql.append(" and ").append(column).append(" in (").append(inValues).append(")");
                        }
                    }
                }
            }
        } catch (Exception e) {
            //输出到日志文件中
            log.error(ErrorUtil.errorInfoToString(e));
        }
    }

    /**
     *
     * @param entityClass 自动拼接实体类
     * @param ignoreProperties 动态参数  忽略拼接的字段
     * @return sql
     */
    public static StringBuilder appendFields(Class<?> entityClass, String... ignoreProperties) {
        StringBuilder sql=new StringBuilder();
        List<String> ignoreList=Arrays.asList(ignoreProperties);
        sql.append("select ");

        for (Field field : entityClass.getDeclaredFields()) {
            //获取授权
            field.setAccessible(true);
            String fieldName=field.getName();//属性名称

            //非临时字段、非忽略字段
            if (!field.isAnnotationPresent(Transient.class) && !ignoreList.contains(fieldName)) {
                //拼接查询字段  驼峰属性转下划线
                sql.append(SqlUtil.translate(fieldName)).append(" ").append(",");
            }
        }
        //处理逗号(删除最后一个字符)
        sql.deleteCharAt(sql.length() - 1);

        String tableName=entityClass.getAnnotation(Table.class).name();
        sql.append("from ").append(tableName).append(" where '1'='1' ");
        return sql;
    }

    /**
     * 拼接排序SQL
     *
     * @param pageCondition 继承了PageCondition分页条件的Vo类
     * @param sql    待拼接的SQL
     */
    public static void orderByColumn(PageCondition pageCondition, StringBuilder sql) {
        String sidx=pageCondition.getSidx();
        String sord=pageCondition.getSord();

        if (!StringUtils.isEmpty(sidx)) {
            //1.获取Bean
            BeanWrapper srcBean=new BeanWrapperImpl(pageCondition);
            //2.获取Bean的属性描述
            PropertyDescriptor[] pds=srcBean.getPropertyDescriptors();
            //3.获取符合的排序字段名
            for (PropertyDescriptor p : pds) {
                String propertyName=p.getName();
                if (sidx.equals(propertyName)) {
                    sql.append(" order by ").append(translate(sidx)).append("desc".equalsIgnoreCase(sord) ? " desc" : " asc");
                }
            }
        }
    }

    /**
     * 实体属性转表字段,驼峰属性转下划线,并全部转小写
     */
    private static String translate(String fieldName){
        return new PropertyNamingStrategy.SnakeCaseStrategy().translate(fieldName).toLowerCase();
    }

    /**
     * sql转义
     * 动态拼写SQL,需要进行转义防范SQL注入!
     */
    private static String escapeSql(String str) {
        if (str==null) {
            return null;
        }
        StringBuilder sb=new StringBuilder();
        for (int i=0; i < str.length(); i++) {
            char src=str.charAt(i);
            switch (src) {
                case '\'':
                    sb.append("''");// hibernate转义多个单引号必须用两个单引号
                    break;
                case '\"':
                case '\\':
                    sb.append('\\');
                default:
                    sb.append(src);
                    break;
            }
        }
        return sb.toString();
    }
}


  如何使用?比如我现在有entity映射类SysUser.java,以及对应的Vo类SysUserVo.java

  entity,负责与数据表进行映射,具有共有属性,不应该被业务污染

@Entity
@Table(name="sys_user")
@Data
public class SysUser implements Serializable {
    @Id
    private String userId;//用户id

    private String loginName;//登录名

    private String userName;//用户名称

    private String password;//登录密码

    private String valid;//软删除标识,Y/N

    private String limitedIp;//限制允许登录的IP集合

    private Date expiredTime;//账号失效时间,超过时间将不能登录系统

    private Date lastChangePwdTime;//最近修改密码时间,超出时间间隔,提示用户修改密码

    private String limitMultiLogin;//是否允许账号同一个时刻多人在线,Y/N

    private Date createTime;//创建时间

    private Date updateTime;//修改时间

}


  vo,负责传输数据,如接参、传参,当一个vo不满足多个业务需求时,可以新建多个vo类

@Data
public class SysUserVo extends PageCondition implements Serializable {

    @In(values="userIdList")//in查询
    private String userId;//用户id

    @Like
    private String loginName;//登录名

    private String userName;//用户名称

    private String password;//登录密码

    private String valid;//软删除标识,Y/N

    private String limitedIp;//限制允许登录的IP集合

    private Date expiredTime;//账号失效时间,超过时间将不能登录系统

    private Date lastChangePwdTime;//最近修改密码时间,超出时间间隔,提示用户修改密码

    private String limitMultiLogin;//是否允许账号同一个时刻多人在线,Y/N

    @Between(min="createTimeStart", max="createTimeEnd")//开启区间查询
    private Date createTime;//创建时间

    private Date updateTime;//修改时间

    private String oldPassword;//修改密码时输入的旧密码

    private String newPassword;//修改密码时输入的新密码

    private Date createTimeStart;
    private Date createTimeEnd;
    private List<String> userIdList;
}


  main测试

public static void main(String[] args) {
        SqlUtil.sqlType="com.mysql.cj.jdbc.Driver";
        SysUserVo sysUserVo=new SysUserVo();
        //like查询
        sysUserVo.setLoginName("张三");

        //日期区域查询
        sysUserVo.setCreateTimeStart(new Date());
        sysUserVo.setCreateTimeEnd(new Date());

        //in查询
        ArrayList<String> userIds=new ArrayList<>();
        userIds.add("1");
        userIds.add("2");
        sysUserVo.setUserIdList(userIds);

        //排序  asc desc
        sysUserVo.setSidx("createTime");
        sysUserVo.setSord("desc");

        //根据实体、Vo直接拼接全部SQL
        StringBuilder sql=SqlUtil.joinSqlByEntityAndVo(SysUser.class,sysUserVo);
        System.out.println(sql.toString());
    }


  结果

15:10:21.457 [main] DEBUG cn.huanzi.qch.baseadmin.util.SqlUtil - entity中没有这个字段,拼接查询SQL直接跳过:oldPassword
15:10:21.467 [main] DEBUG cn.huanzi.qch.baseadmin.util.SqlUtil - entity中没有这个字段,拼接查询SQL直接跳过:newPassword
15:10:21.467 [main] DEBUG cn.huanzi.qch.baseadmin.util.SqlUtil - entity中没有这个字段,拼接查询SQL直接跳过:createTimeStart
15:10:21.467 [main] DEBUG cn.huanzi.qch.baseadmin.util.SqlUtil - entity中没有这个字段,拼接查询SQL直接跳过:createTimeEnd
15:10:21.467 [main] DEBUG cn.huanzi.qch.baseadmin.util.SqlUtil - entity中没有这个字段,拼接查询SQL直接跳过:userIdList
select user_id ,login_name ,user_name ,password ,valid ,limited_ip ,expired_time ,last_change_pwd_time ,limit_multi_login ,create_time ,update_time from sys_user where '1'='1'  and user_id in ('1','2') and login_name like '%张三%' and create_time > str_to_date( '2020-10-10 15:10:21','%Y-%m-%d %H:%i:%s') and create_time < str_to_date( '2020-10-10 15:10:21','%Y-%m-%d %H:%i:%s') order by create_time desc


  美化后的SQL




  业务冲突问题,场景重现:

  A只要对loginName使用@Like查询,其他全部是等值查询,但是B想要对userName使用@Like查询,其他全部是等值查询,这时候两者的业务需要体现在同一个Vo类中就冲突了,如果我们在entity映射类中使用自定义注解,这种冲突情况就不能很好地解决,因为映射类应当有且只有一个,但现在改成在Vo类中使用自定义注解,我们可以新建多个对应Vo类来解决这种冲突问题



  代码开源

  注:本文的的代码,在base-admin项目的SqlUtil.java工具类中


  代码已经开源、托管到我的GitHub、码云:

  GitHub:https://github.com/huanzi-qch/base-admin

  码云:https://gitee.com/huanzi-qch/base-admin


版权声明

作者:huanzi-qch

出处:https://www.cnblogs.com/huanzi-qch

若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.

态SQL

官方文档:https://mybatis.org/mybatis-3/zh/dynamic-sql.html

介绍

什么是动态SQL:动态SQL指的是根据不同的查询条件 , 生成不同的Sql语句.

官网描述:
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你
就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意
去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语
句中的强大的动态 SQL 语言得以改进这种情形。
动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有
很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。
MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。
-------------------------------
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
-------------------------------

我们之前写的 SQL 语句都比较简单,如果有比较复杂的业务,我们需要写复杂的 SQL 语句,往往需 要拼接,而拼接 SQL ,稍微不注意,由于引号,空格等缺失可能都会导致错误。

那么怎么去解决这个问题呢?这就要使用 mybatis 动态SQL,通过 if, choose, when, otherwise, trim, where, set, foreach等标签,可组合成非常灵活的SQL语句,从而在提高 SQL 语句的准确性的同 时,也大大提高了开发人员的效率。

搭建环境

新建一个数据库表:blog

字段:id,title,author,create_time,views

CREATE TABLE `blog` (
`id` varchar(50) NOT NULL COMMENT '博客id',
`title` varchar(100) NOT NULL COMMENT '博客标题',
`author` varchar(30) NOT NULL COMMENT '博客作者',
`create_time` datetime NOT NULL COMMENT '创建时间',
`views` int(30) NOT NULL COMMENT '浏览量'
) ENGINE=InnoDB DEFAULT CHARSET=utf8

创建Mybatis基础工程


IDutil工具类

public class IDUtil {
public static String genId(){
return UUID.randomUUID().toString().replaceAll("-","");
}
}

实体类编写 【注意set方法作用】

import java.util.Date;
public class Blog {
private String id;
private String title;
private String author;
private Date createTime;
private int views;
//set,get....
}

编写Mapper接口及xml文件

public interface BlogMapper {
}
<?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.xiaohan.mapper.BlogMapper">
</mapper>

mybatis核心配置文件,下划线驼峰自动转换

<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!--注册Mapper.xml-->
<mappers>
<mapper resource="mapper/BlogMapper.xml"/>
</mappers>

插入初始数据

编写接口

//新增一个博客
int addBlog(Blog blog);

sql配置文件

<insert id="addBlog" parameterType="blog">
insert into blog (id, title, author, create_time, views)
values (#{id},#{title},#{author},#{createTime},#{views});
</insert>

初始化博客方法

@Test
public void addInitBlog(){
SqlSession session=MybatisUtils.getSession();
BlogMapper mapper=session.getMapper(BlogMapper.class);
Blog blog=new Blog();
blog.setId(IDUtil.genId());
blog.setTitle("Mybatis如此简单");
blog.setAuthor("小涵");
blog.setCreateTime(new Date());
blog.setViews(9999);
mapper.addBlog(blog);
blog.setId(IDUtil.genId());
blog.setTitle("Java如此简单");
mapper.addBlog(blog);
blog.setId(IDUtil.genId());
blog.setTitle("Spring如此简单");
mapper.addBlog(blog);
blog.setId(IDUtil.genId());
blog.setTitle("微服务如此简单");
mapper.addBlog(blog);
session.close();
}

初始化数据完毕!接下来动态sql要来了,做好准备!!!

if 语句

需求:根据作者名字和博客名字来查询博客!如果作者名字为空,那么只根据博客名字查询,反之,则 根据作者名来查询

编写接口类

//需求1
List<Blog> queryBlogIf(Map map);

编写SQL语句

<!--需求1:
根据作者名字和博客名字来查询博客!
如果作者名字为空,那么只根据博客名字查询,反之,则根据作者名来查询
select * from blog where title=#{title} and author=#{author}
-->
<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from blog where
<if test="title !=null">
title=#{title}
</if>
<if test="author !=null">
and author=#{author}
</if>
</select>

测试

@Test
public void testQueryBlogIf(){
SqlSession session=MybatisUtils.getSession();
BlogMapper mapper=session.getMapper(BlogMapper.class);
HashMap<String, String> map=new HashMap<String, String>();
map.put("title","Mybatis如此简单");
map.put("author","小涵");
List<Blog> blogs=mapper.queryBlogIf(map);
System.out.println(blogs);
session.close();
}

这样写我们可以看到,如果 author 等于 null,那么查询语句为 select * from user where title=#{title}, 但是如果title为空呢?那么查询语句为 select * from user where and author=#{author},这是错误的 SQL 语句,如何解决呢?请看下面的 where 语句!

Where

修改上面的SQL语句;

<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from blog
<where>
<if test="title !=null">
title=#{title}
</if>
<if test="author !=null">
and author=#{author}
</if>
</where>
</select>

这个“where”标签会知道如果它包含的标签中有返回值的话,它就插入一个‘where’。此外,如果标签返回的内容是以AND 或OR 开头的,则它会剔除掉。【这是我们使用的最多的案例】

Set

同理,上面的对于查询 SQL 语句包含 where 关键字,如果在进行更新操作的时候,含有 set 关键词, 我们怎么处理呢?

1. 编写接口方法

int updateBlog(Map map);

2.sql配置文件

<!--注意set是用的逗号隔开-->
<update id="updateBlog" parameterType="map">
update blog
<set>
<if test="title !=null">
title=#{title},
</if>
<if test="author !=null">
author=#{author}
</if>
</set>
where id=#{id};
</update>

3.测试

@Test
public void testUpdateBlog(){
SqlSession session=MybatisUtils.getSession();
BlogMapper mapper=session.getMapper(BlogMapper.class);
HashMap<String, String> map=new HashMap<String, String>();
map.put("title","动态SQL");
map.put("author","秦疆");
map.put("id","9d6a763f5e1347cebda43e2a32687a77");
mapper.updateBlog(map);
session.close();
}

choose语句

有时候,我们不想用到所有的查询条件,只想选择其中的一个,查询条件有一个满足即可,使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句

1. 编写接口方法

List<Blog> queryBlogChoose(Map map);

2. sql配置文件

<select id="queryBlogChoose" parameterType="map" resultType="blog">
select * from blog
<where>
<choose>
<when test="title !=null">
title=#{title}
</when>
<when test="author !=null">
and author=#{author}
</when>
<otherwise>
and views=#{views}
</otherwise>
</choose>
</where>
</select>

3. 测试类

@Test
public void testQueryBlogChoose(){
SqlSession session=MybatisUtils.getSession();
BlogMapper mapper=session.getMapper(BlogMapper.class);
HashMap<String, Object> map=new HashMap<String, Object>();
map.put("title","Java如此简单");
map.put("author","小涵");
map.put("views",9999);
List<Blog> blogs=mapper.queryBlogChoose(map);
System.out.println(blogs);
session.close();
}

SQL片段

有时候可能某个 sql 语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽 取出来,然后使用时直接调用。

提取SQL片段:

<sql id="if-title-author">
<if test="title !=null">
title=#{title}
</if>
<if test="author !=null">
and author=#{author}
</if>
</sql>

引用SQL片段:

<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from blog
<where>
<!-- 引用 sql 片段,如果refid 指定的不在本文件中,那么需要在前面加上 namespace
-->
<include refid="if-title-author"></include>
<!-- 在这里还可以引用其他的 sql 片段 -->
</where>
</select>

注意:

①、最好基于 单表来定义 sql 片段,提高片段的可重用性

②、在 sql 片段中不要包括 where

Foreach

将数据库中前三个数据的id修改为1,2,3;

需求:我们需要查询 blog 表中 id 分别为1,2,3的博客信息

1. 编写接口

List<Blog> queryBlogForeach(Map map);

2. 编写SQL语句

<select id="queryBlogForeach" parameterType="map" resultType="blog">
select * from blog
<where>
<!--
collection:指定输入对象中的集合属性
item:每次遍历生成的对象
open:开始遍历时的拼接字符串
close:结束时拼接的字符串
separator:遍历对象之间需要拼接的字符串
select * from blog where 1=1 and (id=1 or id=2 or id=3)
-->
<foreach collection="ids" item="id" open="and (" close=")"
separator="or">
id=#{id}
</foreach>
</where>
</select>

3. 测试

@Test
public void testQueryBlogForeach(){
SqlSession session=MybatisUtils.getSession();
BlogMapper mapper=session.getMapper(BlogMapper.class);
HashMap map=new HashMap();
List<Integer> ids=new ArrayList<Integer>();
ids.add(1);
ids.add(2);
ids.add(3);
map.put("ids",ids);
List<Blog> blogs=mapper.queryBlogForeach(map);
System.out.println(blogs);
session.close();
}


小结:其实动态 sql 语句的编写往往就是一个拼接的问题,为了保证拼接准确,我们最好首先要写原生 的 sql 语句出来,然后在通过 mybatis 动态sql 对照着改,防止出错。多在实践中使用才是熟练掌握它的技巧

哈哈,终于总结完啦,以上就是mybatis基本动态sql的使用啦,后续还会持续更新动态sql的其他用法,如果对小涵的总结有不同的看法,直接评论就好啦!别忘了点波关注、转发哦!

、概述

SQL是操作数据库数据的结构化查询语言,网页的应用数据和后台数据库中的数据进行交互时会采用SQL。而SQL注入是将Web页面的原URL、表单域或数据包输入的参数,修改拼接成SQL语句,传递给Web服务器,进而传给数据库服务器以执行数据库命令。如Web应用程序的开发人员对用户所输入的数据或cookie等内容不进行过滤或验证(即存在注入点)就直接传输给数据库,就可能导致拼接的SQL被执行,获取对数据库的信息以及提权,发生SQL注入攻击

SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。

SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。



二、特点

  • 广泛性

任何一个基于SQL语言的数据库都可能被攻击,很多开发人员在编写Web应用程序时未对从输入参数、Web表单、cookie等接受到的值进行规范性验证和检测,通常会出现SQL注入漏洞。

  • 隐蔽性

SQL注入语句一般都嵌入在普通的HTTP请求中,很难与正常语句区分开,所以当前许多防火墙都无法识别予以警告,而且SQL注入变种极多,攻击者可以调整攻击的参数,所以使用传统的方法防御SQL注入效果非常不理想。

  • 危害大

攻击者通过SQL注入获取到服务器的库名、表名、字段名,从而获取到整个服务器中的数据,对网站用户的数据安全有极大的威胁。攻击者也可以通过获取到的数据,得到后台管理员的密码,然后对网页页面进行恶意篡改。这样不仅对数据库信息安全造成严重威胁,对整个数据库系统安全也影响重大。

  • 操作方便

互联网上有很多SQL注入工具,简单易学,攻击过程简单,不需要专业知识也能自如运用。



三、SQL注入的原理

SQL注入攻击是通过操作输入来修改SQL语句,用以达到执行代码对WEB服务器进行攻击的方法。简单的说就是在post/getweb表单、输入域名或页面请求的查询字符串中插入SQL命令,最终使web服务器执行恶意命令的过程。在网站开发过程中,开发人员使用动态字符串构造SQL语句,用来创建所需的应用,这种情况下SQL语句在程序的执行过程中被动态的构造使用,可以根据不同的条件产生不同的SQL语句,比如需要根据不同的要求来查询数据库中的字段。这样的开发过程其实为SQL注入攻击留下了很多的可乘之机。


我们举一个万能钥匙的例子来说明其原理:

考虑以下简单的管理员登录表单:

<form action="/login" method="POST">
    <p>Username: <input type="text" name="username" /></p>
    <p>Password: <input type="password" name="password" /></p>
    <p><input type="submit" value="登陆" /></p>
</form>

后端的 SQL 语句可能是如下这样的:

let querySQL=`
    SELECT *
    FROM user
    WHERE username='${username}'
    AND psw='${password}'
`;
// 接下来就是执行 sql 语句...

目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 admin' --,密码随意输入,就可以直接登入系统了。WFT!----这就是SQL注入

我们之前预想的SQL 语句是:

SELECT * FROM user WHERE username='admin' AND psw='password'

但是恶意攻击者用奇怪用户名将你的 SQL 语句变成了如下形式:

SELECT * FROM user WHERE username='admin' --' AND psw='xxxx'

在 SQL 中,' --是闭合和注释的意思,-- 是注释后面的内容的意思,所以查询语句就变成了:

SELECT * FROM user WHERE username='admin'

所谓的万能密码,本质上就是SQL注入的一种利用方式。恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。

一次SQL注入的过程包括以下几个过程:

  • 获取用户请求参数
  • 拼接到代码当中
  • SQL语句按照我们构造参数的语义执行成功

SQL注入的必备条件:

1.可以控制输入的数据

2.服务器要执行的代码拼接了控制的数据

我们会发现SQL注入流程中与正常请求服务器类似,只是黑客控制了数据,构造了SQL查询,而正常的请求不会SQL查询这一步,SQL注入的本质:数据和代码未分离,即数据当做了代码来执行。



四、攻击特点

SQL注入攻击是目前web应用网络攻击中最常见的手段之一,安全风险较高,在一定程度上超过缓冲区溢出漏洞,而市场上的防火墙又不能对SQL注入漏洞进行有效的检测和防范。防火墙为了使正常网络应用程序访问服务器端的数据,必须允许从互联网到Web服务器的正向连接,因此一旦web网络应用程序存在注入漏洞,攻击者就可以获取访问数据库的权利进而获得数据库所在服务器的访问权在某些情况下,SQL注入攻击的风险要高于缓冲区溢出漏洞等所有其他漏洞。SQL注入攻击普遍存在范围广、实现容易、破坏性大等特点。 [8]

SQL注入攻击者在HTTP请求中输入含有恶意构造且语法合法的SQL语句,只要应用程序中没有做严格的处理(例如校验或预拼接),那么就会出现SQL注入漏洞危险,目前以PHP、Perl、Cold Fusion Management等技术与Oracle、SQLServer、Sybase、DB2等数据管理系统相结合的Web应用程序都发现有SQL注入漏洞。 [8]

SQL注入技术公布后不久,互联网上出现了很多例如教主的HDSI、NBSI、明小子的Domain等SQL注入工具,对那些存在SQL注入的网站以及Web应用程序进行攻击,很容易就可以获取其服务器的控制权。



五、危害

①获取数据库信息

    • 管理员后台用户名和密码
    • 获取其他数据库敏感信息:用户名、密码、手机号码、身份证、银行卡信息……
    • 整个数据库:脱裤

②获取服务器权限

③植入Webshell,获取服务器后门

④读取服务器敏感文件



六、如何防御

  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。
mysql.query(`SELECT * FROM user WHERE username=? AND psw=?`, [username, psw]);
  • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。
  • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。
  • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢?



参考:

https://juejin.cn/post/6844903772930441230

https://zoumiaojiang.com/article/common-web-security/