主题

  公司在DAO层使用的框架是Spring Data JPA,这个框架很好用,基本不需要自己写SQL或者HQL就能完成大部分事情,但是偶尔有一些复杂的查询还是需要自己手写原生的Native SQL或者HQL.同时公司前端界面使用的是jquery miniui框架,并且自己进行了一些封装.

  当界面上查询条件比较多的时候,需要动态拼接查询条件,使用JPA的话可以通过CriteriaQuery进行面向对象的方式进行查询,但是偶尔有时候又要用到HQL或者SQL,毕竟比CriteriaQuery简单很多.而两者似乎不能很好的结合(我还没见过将两者混用的,不过可能是我CriteriaQuery使用的比较少的原因).

  这也是我写这篇文章的原因,记录分享一下公司的解决办法(公司的策略也不是完美的,只能适用于简单的查询,复杂的情况还是不支持的,但是也不失为一种解决办法)

原理

  公司动态查询的原理是使用HQL或者SQL,在这个基本select语句上动态拼接where条件...公司认为用户在界面上动态选择条件的时候,select 的表基本是不会变化的,有变化的是where字句里的条件.所以需要对用户的输入和where子句里的条件进行封装.

  DefaultPage:这个类是公司对前端界面用户输入的查询条件,比如分页信息等等进行的封装.(当然还有其他很多类..只是这个是最主要的,怎么封装的不是这篇文章的主题)

  SearchCriteria:这个是公司对查询条件where子句的封装,它可以通过一些方法转化成where子句里SQL字符串..

  最主要的就是以上2个类,核心思想就是封装用户输入的查询信息到DefaultPage,从DefaultPage中取得SearchCriteria,将SearchCriteria转化成字符串形式,拼接在基础的SQL(HQL)之上,形成动态的SQL来查询数据.

主要实现

SearchCriteria

SearchCriteria是对where子句的封装,SearchCriteria中包含很多个小的SubCriteria,SubCriteria是对where子句里每个条件的封装.

比如有个where子句是:

  1. where 1=1 and user.username = 'user1' and user.state in ('','');

那整个where语句是1个SearchCriteria

and user.username = 'user1'是第一个SubCriteria

and user.state in ('0','1')是第二个SubCriteria

1=1是为了拼接方便,就算没有查询条件也拼接where条件而增加的一个拼接默认条件

  1. public SubCriteria(String attName, EOperator operator, Object value, ERelation relation, String tabelAlias) {
  2. this.attName = attName;
  3. this.operator = operator;
  4. this.attValue = value;
  5. this.relation = relation;
  6. this.tableAliasName = tabelAlias;
  7. }

从SubCriteria的构造方法中可以看出,如果一个SubCriteria对应的是and user.username = 'user1'

那attName就是username

operator就是=

value就是'user1'

relation就是and

tableAlias就是user

1个SearchCriteria中肯定会含有N个SubCriteria

  1. List<SubCriteria> list = new ArrayList<SubCriteria>();

查询的数据库结果的方法如下:

  1. @Override
  2. public <D> List<D> executeDynamicQuerySql(String sql, SearchCriteria criteria, Class<? extends D> targetClass,
  3. Map<String, Object> customParams) {
  4. Map<String, Object> paramMap = new HashMap<String, Object>();
  5. String handledSql = calculateDynamicSql(sql, criteria, paramMap);
  6. mergeCustomParams(paramMap, customParams);
  7. return NativeSqlExecutor.executeQuerySql(getEntityManager(), handledSql, paramMap, targetClass);
  8. }

sql的格式就是类似于 select * from user user %Where_Clause% and user.yxbz = :yxbz     (yxbz是有效标志的意思)

criteria就是界面上动态选择的查询条件和值的封装

targetClass不重要,只是为了把结果封装成对象时候,指定要封装到哪个类型的对象里.(数据库返回结果封装到对象也是公司自己封装的代码)

customParams 里存是sql里一些占位符参数的键值对,比如key=yxbz,value='Y'

第5行代码calculateDynamicSql将Criteria转化成的SQL拼接到传入的sql中,替换掉%Where_Clause%,在把Criteria中的占位符键值对放到paramMap中

第6行,将paramMap与传入的customParams合并,得到一个合并的map,即是把customParams的键值对放到paramMap中.

第7行就是常规的调用JPA的方法,把生成的动态的SQL与参数Map传给JPA去执行,根据targetClass将返回的结果封装成对象.

calcilateDynamicSql具体步骤如下:

  1. private String calculateDynamicSql(String sql, SearchCriteria criteria, Map<String, Object> paramMap) {
  2. String searchSql = calculateSearchDynamicSql(sql, criteria, paramMap);
  3. return calculateOrderDynamicSql(searchSql, criteria);
  4. }
  5.  
  6. private String calculateSearchDynamicSql(String sql, SearchCriteria criteria, Map<String, Object> paramMap) {
  7. StringBuilder whereClause = new StringBuilder(" WHERE 1=1 ");
  8. int index = paramMap.keySet().size() + 1;
  9. for (SubCriteria subCriteria : criteria.getCreteriaList()) {
  10. whereClause.append(subCriteria.getRelation().getCode());
  11. whereClause.append(StringUtils.isEmpty(subCriteria.getTableAliasName()) ? "" : subCriteria
  12. .getTableAliasName() + ".");
  13. whereClause.append(subCriteria.getAttName());
  14. whereClause.append(" ");
  15. whereClause.append(subCriteria.getOperator().getCode());
  16. whereClause.append(" ");
  17. if (EOperator.IN == subCriteria.getOperator()) {
  18.  
  19. @SuppressWarnings("unchecked")
  20. List<Object> paramValues = (List<Object>) subCriteria.getAttValue();
  21. whereClause.append("(");
  22. for (int i = 0; i < paramValues.size(); i++) {
  23. whereClause.append(PLACEHOLDER);
  24. String key = PARAM_PREFIX + (index++);
  25. whereClause.append(key);
  26. if (i != paramValues.size() - 1) {
  27. whereClause.append(",");
  28. }
  29. paramMap.put(key, paramValues.get(i));
  30. }
  31. whereClause.append(")");
  32. } else {
  33. whereClause.append(PLACEHOLDER);
  34. String key = PARAM_PREFIX + (index++);
  35. whereClause.append(key);
  36. whereClause.append(" ");
  37. paramMap.put(key, subCriteria.getAttValue());
  38. }
  39. }
  40. return sql.replace("%WHERE_CLAUSE%", whereClause.toString());
  41. }

calculateDynamicSql中又分为2个步骤,先根据SearchCriteria计算出where字符串,再根据SearchCriteria计算order by子句...order by子句比where子句简单很多,原理也差不多...就不介绍了..主要看calculateSearchDynamicSql这个计算where子句的方法步骤主要是:

1.先拼接where 1=1 这是为了简化问题,防止用户什么都不选的时候不用拼接where子句的问题,就算动态SQL里什么where条件都不写,也会拼接where 1=1 这个条件来简化问题.

2.有了步骤1可以保证一定拼接了where字符串,后续只要把SubCriteria转化成字符串拼接到where子句中就OK了. 拼接方法如前面介绍SubCriteria所说,就是把SubCriteria中的属性一个一个取出来拼接.唯一有点区别的就是如果SubCriteria中EOperator是in操作符,那传过来的参数值是个list而不是一个String...

mergeCustomParams(paramMap, customParams)方法

拼接完了SQL,就需要把SearchCriteria中的占位符键值对与用户传入的键值对合并.

  1. private void mergeCustomParams(Map<String, Object> paramMap, Map<String, Object> customParams) {
  2. if (null == customParams || null == paramMap) {
  3. return;
  4. }
  5. for (Map.Entry<String, Object> entry : customParams.entrySet()) {
  6. if (null != entry.getKey()) {
  7. if (paramMap.containsKey(entry.getKey())) {
  8. throw new IllegalArgumentException("动态SQL不允许自定义占位符以 'P_' 开始,该类占位符用于 searchCriteria动态生成。");
  9. }
  10. paramMap.put(entry.getKey(), entry.getValue());
  11. }
  12. }
  13. }

当然其中键值对可能会有同名的...那就报错...

executeQuerySql方法

  1. public static <D> List<D> executeQuerySql(EntityManager entityManager, String sql, Map<String, Object> paramMap,
  2. Class<? extends D> targetClass) {
  3.  
  4. LOGGER.debug("Execute native query sql {} with parameters {} for target {}", sql, paramMap, targetClass);
  5. Query query = entityManager.createNativeQuery(sql);
  6. if (DbColumnMapper.isNamedMapping(targetClass)) {
  7. query.unwrap(SQLQuery.class).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
  8. }
  9. if (paramMap != null) {
  10. for (Entry<String, Object> entry : paramMap.entrySet()) {
  11. query.setParameter(entry.getKey(), entry.getValue());
  12. }
  13. }
  14. return DbColumnMapper.resultMapping(query.getResultList(), targetClass);
  15. }

核心就是:

1.query.setParameter(entry.getKey(), entry.getValue());根据传入的键值对设置到占位符中

2.query.getResultList()查询结果..其他很多代码是用于把query.getResultList()的结果封装成对象用的..主要方法是在targetClass的类的Field上使用注解标注.

DefaultPage

公司对前后台请求与参数都进行了封装,前面写过一篇文章,介绍了公司的基本思路(当然实现肯定不一样)

http://www.cnblogs.com/abcwt112/p/5169250.html

所以这里不再详细介绍前台用户选择的那些查询条件怎么封装到DefaultPage里了...

我们来看看DefaultPage如何生成SearchCriteria的:

  1. private SearchCriteria buildSearchCriteria(String buildSearchType) {
  2. SearchCriteria search = new SearchCriteria();
  3. operationMap = this.getOperationMap();
  4. try {
  5. for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
  6. String paramKey = entry.getKey();
  7. Object value = parameterMap.get(paramKey);
  8. boolean isString = value instanceof String;
  9.  
  10. if (value != null) {
  11. if (isString) {
  12. if (StringUtils.isNotEmpty((String) value)) {
  13. search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey),
  14. operationMap.get(paramKey));
  15. this.parameterValues.add(value);
  16. }
  17. } else {
  18. search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey),
  19. operationMap.get(paramKey));
  20. this.parameterValues.add(value);
  21. }
  22.  
  23. }
  24. }
  25. for (SearchOrder order : orderBy) {
  26. if (StringUtils.isNotEmpty(order.getOrderName())) {
  27. SearchOrder realOrder = new SearchOrder(this.getColumnName(order.getOrderName(), buildSearchType),
  28. order.isAsc());
  29. search.getOrderByList().add(realOrder);
  30. }
  31. }
  32.  
  33. } catch (Exception ex) {
  34. throw new SystemException(SystemException.REQUEST_EXCEPTION, ex, ex.getMessage());
  35. }
  36. return search;
  37. }

buildSearchType有2种,1种最常用的就是生成我们这里一般的SQL查询的searchCriteria,还有一种适用于SpringDataJpa,用于生成适用于Specification接口的SearchCriteria用的...

生成SearchCriteria主要是为了生成SubCriteria,通过调用SearchCriteria的add方法直接在生成一个SubCriteria并放到SearchCriteria的List<SubCriteria>成员域中.

  1. search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey),
  2. operationMap.get(paramKey));
  1. public void add(String attribute, Object value, EOperator operator) {
  2. if (attribute == null || operator == null) {
  3. return;
  4. }
  5. list.add(new SubCriteria(attribute, operator, value));
  6. }

search.add的第一个参数是attribute就是where user.username = :username中的username

getColumn方法如下:

  1. private String getColumnName(String paramKey, String buildType) throws Exception {// NOSONAR
  2. if (paramKey.indexOf(':') > 0) {
  3. String[] keys = paramKey.split(":");
  4. if (null != keys && keys.length == 2 && cmpClass != null) {
  5. Field field = cmpClass.getDeclaredField(keys[0]);
  6. if (null != field) {
  7. StringBuilder sb = new StringBuilder();
  8. sb.append(keys[0]);
  9. sb.append('.');
  10. if (BUILDTYPE_NATIVE.equalsIgnoreCase(buildType)) {
  11. sb.append(getColumnName(keys[1], buildType, field.getClass()));
  12. } else {
  13. sb.append(keys[1]);
  14. }
  15. return sb.toString();
  16. }
  17. }
  18. }
  19. return getColumnName(paramKey, buildType, cmpClass);
  20. }

大多数情况下是直接调用19行的getColumnName

  1. private String getColumnName(String paramKey, String buildType, Class<?> clazz) throws Exception {// NOSONAR
  2. String columnName = paramKey;
  3. if (paramKey.endsWith("_start")) {
  4. columnName = paramKey.replaceAll("_start", "");
  5. }
  6. if (paramKey.endsWith("_end")) {
  7. columnName = paramKey.replaceAll("_end", "");
  8. }
  9. if (clazz != null && BUILDTYPE_NATIVE.equalsIgnoreCase(buildType)) {
  10. Field field = clazz.getDeclaredField(columnName);
  11. Column column = field.getAnnotation(Column.class);
  12. if (column != null) {
  13. columnName = column.name();
  14. }
  15. }
  16. return columnName;
  17. }

clazz是前台数据传过来肯定会封装到一个接受对象上,如果那个对象的field上用了注解@Column,那就取注解里写的name作为attribute的name,否则就取前台传过来的参数值作为attribute.

这是因为jpa的注解@column可以让实体类里的字段映射到数据库中表的字段,但是两者的名字可以不同,将SearchCriteria转化成SQL的时候用要使用数据库中的字段名而不是实体类中的属性名.

这样就能构造出一个SearchCriteria了.

以上便是公司对DAO层动态SQL的主要封装逻辑..在查询条件不复杂的情况下还算好用...

但是查询条件比较复杂的话就有点力不从心了..因为SearchCriteria里只有一个SubCriteria的list,而SubCriteria中不能包含SubCriteria...所以像 where (a.b = '1' or a.c = '2') and ....

这样的查询就做不出来...

分享公司DAO层动态SQL的一些封装的更多相关文章

  1. 分享公司DAO层数据库结果映射到对象的方法

    主题 前面写过一篇文章,分享了公司是怎么动态封装SQL查询条件的(http://www.cnblogs.com/abcwt112/p/5874401.html). 里面提到数据库查询结果二维数组最后是 ...

  2. Dao层向sql语句传递多个参数

    手动封装: serviceImpl层 Map<String, Object> params = new HashMap<String, Object>(2);params.pu ...

  3. MyBatis进阶--接口代理方式实现Dao 和动态SQL

    MyBatis接口代理方式实现Dao层 接口代理方式-实现规则 传统方式实现Dao层,我们既要写接口.还要写实现类.而MyBatis框架可以帮助我们省略写Dao层接口实现类的步骤.程序员只需要编写接口 ...

  4. Dao层的sql语句

    2018-08-12     21:33:43 反思:在数据库执行的时候,sql语句是正确的,复制到方法中,执行出错   因为把限定条件改为?时,把左括号删掉了,sql语句报错 改正:一定要确保sql ...

  5. 动态sql构建的过程

    基本原理:使用xsqlbuilder框架完成动态sql的构建. 基本流程:使用WebUtils.getParametersStartingWith(ServletActionContext.getRe ...

  6. 关于mysql,需要掌握的基础(二):JDBC和DAO层

    ​ 目录 关于mysql,需要掌握的基础(二):JDBC和DAO层 1.了解jdbc是什么? 2.加载注册驱动:为什么Class.forName("com.mysql.jdbc.Driver ...

  7. 最常用的动态sql语句梳理——分享给使用Mybatis的小伙伴们!

    公司项目中一直使用Mybatis作为持久层框架,自然,动态sql写得也比较多了,最常见的莫过于在查询语句中使用if标签来动态地改变过滤条件了.Mybatis的强大特性之一便是它的动态sql,免除了拼接 ...

  8. Mybatis进阶学习笔记——动态代理方式开发Dao接口、Dao层(推荐第二种)

    1.原始方法开发Dao Dao接口 package cn.sm1234.dao; import java.util.List; import cn.sm1234.domain.Customer; pu ...

  9. MyBatis开发Dao层的两种方式(Mapper动态代理方式)

    MyBatis开发原始Dao层请阅读我的上一篇博客:MyBatis开发Dao层的两种方式(原始Dao层开发) 接上一篇博客继续介绍MyBatis开发Dao层的第二种方式:Mapper动态代理方式 Ma ...

随机推荐

  1. javascript-模板方法模式-提示框归一化插件

    模板方法模式笔记   父类中定义一组算法操作骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时可重新定义算法中某些实现步骤   实例:弹出框归一化插件 css样式 ;width ...

  2. redis-cli中那些或许我们还不知道的一些实用小功能

    玩过redis的朋友都知道,redis中有一个叫做redis-cli的小工具,我们可以利用它在test和develop环境下进行高效的模拟测试,然而在现实环境中, 我们只知道直接键入redis-cli ...

  3. SQL Server 2008 R2——当前日期下,一年前数据的统计值

    =================================版权声明================================= 版权声明:原创文章 谢绝转载  请通过右侧公告中的“联系邮 ...

  4. Apache服务停止:信号灯超时时间已到,指定的网络名不再可用

    环境说明:Apache2.4.10,Windows Server 2008 R2 问题说明: apache服务用于下载文件,但是在运行一段时间后,突然挂了. 其错误提示如下所示: [error] (7 ...

  5. 《WePayUI组件设计的秘密》——2016年第一届前端体验大会分享

    本文是博主参加第一届前端体验大会 | 物勒工名做的分享<WePayUI组件设计的秘密>,内容主要分为2个部分: 一.浅析UI库/框架的未来 讨论的UI库或者框架,主要包含展示和交互的css ...

  6. ArrayList<E>源码分析

    ArrayList是按照线性表结构实现的 ArrayList的主要继承结构 public class ArrayList<E> extends AbstractList<E> ...

  7. C语言的fopen函数(文件操作/读写)

    头文件:#include <stdio.h> fopen()是一个常用的函数,用来以指定的方式打开文件,其原型为:    FILE * fopen(const char * path, c ...

  8. 如何将网页的title前面的图标替换成自己的图标

    首先要准备自己的图标,图标必须是.ico格式的图片,网上有很多在线工具可以将自己的图片转换成ico格式的图片,这里给大家介绍两个网站 在线ico转换工具:生成的图标是可以选尺寸的,原图片的大小不限制 ...

  9. es-redis

    列出一些redis命令: 免得我不是dba,每次用都得翻看文档,很蛋疼.于是写了个连接脚本 [root@elk-redis-test105 ts]# ls conn-redis.sh [root@el ...

  10. map 函数----filter函数

    # map 函数 l = (1,2,4,5,6,7,8,9,) print(list(map(lambda x:x**2,l)))#使用list类型((map函数(lambda 匿名函数定义x值:x* ...