加餐 —— 扩展 easy-query

说明

在「 操作 SQL 」中说到,个人比较喜欢 ActiveRecord,后续也会补充对 easy-query 的扩展,比如集成 ActiveRecord 的 SQL 模版管理,基于 Map 的 Model Bean。

可能自己最早是开始从 SQL 开始也业务的,不是一开始就接触 ORM,所以个人习惯使用 SQL 进行代码的编写,特别是有连接的时候,虽然 easy-query 对 join 处理已经很好了,但我还是觉得书写比较麻烦。好在 easy-query提供了原生的 SQL 查询的支持,因此只需要增加 SQL 模版的管理功能,就能使用安心的使用 SQL 写查询了。

在使用 Mybatis-Plus 的过程中发现,这个 ORM 框架在多租户是最无感的。因为引入了 jsqlparser,不论是普通的 orm 的 dsl 写法,还是 SQL 写法都不需要关心租户字段。其他的一些 ORM 框架只是关注 dsl 的写法是能租户字段无感的,在写 SQL 时就需要自己处理租户字段,easy-query 也不例外,jfinal 也是如此。

基于 Map 的 Model Bean,或者 ActiveRecord 的一个好处就是对查询结果的动态扩展或者裁剪,当然这都是要配合 Jfinal 自己的 JSON 序列化,否则动态增加的字段是不会被处理的,另外就是 Jfinal 还提供了 keep 和 remove 等方法可以对数据进行裁剪。这样的好处是什么呢?在代码中可以统一使用一套 Modle 进行传递,而不是各种 Vo 和 Dto 的转换。

自己使用 easy-query 的也不是很熟练,所以这次的调整不一定很完整,这里相当于是提供了一个思路供大家。阅读本文的同学根据自己的情况谨慎使用和自行修改。因为本次的代码的集成修改在另一个项目中(依赖有其他项目的依赖,增加了easyquery和easy-query-web 插件),还没有整理到 porpoise-demo 项目中,所以暂时看不了源码。后续剥离项目的依赖后会陆续上传到 porpoise-demo 中。

Enjoy

因为 AtiveRecord 使用了 enjoy ,做了模版管理的基础,因此可以使用引入依赖包的方式引入,然而原生的 enjoy 对 solon 的容器支持问题(https://gitee.com/jfinal/jfinal/pulls/119),solon 对 enjoy 进行了重新编译。在使用的过程中,发现其实单独版本的 enjoy 和 activerecord 还是有些裁剪的。因此自己整理重新 fork 了一个enjoy 和 activerecord 的版本,基于 5.2.2 版本做了些修改,代码托管在 coding。

地址:

https://e.coding.net/goldsyear/allblue/porpoise-jfinal.git

坐标:

implementation(“com.goldsyear:enjoy:1.0.0”)

ActiveRecord

增加 acitverecord 代码

首先 easy-query 提供了 solon 的插件,将 easy-query 的 solon 插件克隆出来。jfinal 的 ActiveRecord 主要 plugin/activerecord 目录。因此先把两个目录整合在一起,修复其中的编译错误,一些缺失不会处理的可以先屏蔽,确保已经被编辑过来。

在这里插入图片描述

初始化 SqlKit

Jfinal 通过 SqlKit 管理 SQL 模版的加载。因此在构建的 easy-qeury 的 bean 对象的时候,一起初始化 SqlKit,加载对应的SQL 模版。

EasyQueryHolder

package com.easy.query.solon.integration.holder;

import com.easy.query.core.api.client.EasyQueryClient;
import com.easy.query.solon.activerecord.sql.SqlKit;
import org.noear.solon.core.VarHolder;

/**
 * create time 2023/7/24 22:19 文件说明
 *
 * @author xuejiaming
 */
public interface EasyQueryHolder {
  String MAIN_CONFIG_NAME = "main";

  /**
   * 获取sqlKet
   *
   * @return
   */
  SqlKit sqlKit();
  
   // 以下代码省略,方便说明修改 ...
}

DbManger

在构建增加 SqlKit 的初始化,这个通过读取配置,获取 SQL 的文件位置并读取.sql的文件进行装在。

/** 构建 */
private static EasyQueryHolder build(BeanWrap bw) {
  // 以上代码省略,方便说明修改 ...

  // 引入对应的sql模版
  SqlKit sqlKit = new SqlKit(configName, solonEasyQueryProperties.getSqlDev());
  String sqlPath = solonEasyQueryProperties.getSqlPath();
  ScanUtil.scan(sqlPath, n -> n.endsWith(".sql")).forEach(sqlKit::addSqlTemplate);
  sqlKit.parseSqlTemplate();
  SqlManager.add(sqlKit);
  EventBus.publish(sqlKit);

  return injectHolderFactory.get().getHolder(configName, easyQueryClient, sqlKit);
}

使用

通过SqlManger(对应DbManger),获取SQL 语法,直接交给 entityQueryable 进行原生的 SQL 查询,而且可以进行类型转换。

/**
 * 通过sql模板,查询数据
 *
 * @param template
 * @param data
 * @param vClass
 * @return
 * @param <V>
 */
public <V> List<V> listAs(String template, Map<?, ?> data, Class<V> vClass) {
  SqlPara sqlPara = SqlManager.getSqlPara(template, data);
  return entityQueryable(sqlPara.getSql(), Arrays.asList(sqlPara.getPara()))
      .select(vClass)
      .toList();
}

SQL 租户

Easy-query 的 DefaultEntityExpressionExecutor 支持在 querySQL 中对原生的SQL 进行处理,可以在这里增加对租户的逻辑的处理。

依赖

api("com.github.jsqlparser:jsqlparser:5.1")

DefaultEntityExpressionExecutorEx

增加租户逻辑的处理,如果存在 TenantLineHandler 的bean 对象,且没有设置忽略租户的时候就进行解析SQL语法,并进行租户的过滤。

package com.easy.query.solon.integration.excutor;

import com.easy.query.core.basic.jdbc.executor.DefaultEntityExpressionExecutor;
import com.easy.query.core.basic.jdbc.executor.ExecutorContext;
import com.easy.query.core.basic.jdbc.executor.ResultMetadata;
import com.easy.query.core.basic.jdbc.parameter.SQLParameter;
import com.easy.query.core.expression.executor.parser.EasyPrepareParser;
import com.easy.query.core.expression.executor.query.ExecutionContextFactory;
import com.easy.query.core.inject.ServiceProvider;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.noear.solon.core.AppContext;

/**
 * @author airhead
 */
@Slf4j
public class DefaultEntityExpressionExecutorEx extends DefaultEntityExpressionExecutor {
  private final ServiceProvider serviceProvider;

  public DefaultEntityExpressionExecutorEx(
      EasyPrepareParser easyPrepareParser,
      ExecutionContextFactory executionContextFactory,
      ServiceProvider serviceProvider) {
    super(easyPrepareParser, executionContextFactory);
    this.serviceProvider = serviceProvider;
  }

  @Override
  public <TR> List<TR> querySQL(
      ExecutorContext executorContext,
      ResultMetadata<TR> resultMetadata,
      String sql,
      List<SQLParameter> sqlParameters) {
    AppContext appContext = serviceProvider.getService(AppContext.class);
    TenantLineHandler tenantLineHandler = appContext.getBean(TenantLineHandler.class);
    if (tenantLineHandler == null || tenantLineHandler.ignoreTenant()) {
      return super.querySQL(executorContext, resultMetadata, sql, sqlParameters);
    }

    sql = tenantLineHandler.parseSql(sql, null);
    return super.querySQL(executorContext, resultMetadata, sql, sqlParameters);
  }
}

TenantLineHandler

package com.easy.query.solon.integration.excutor;

import com.easy.query.solon.integration.parser.SqlParser;
import com.easy.query.solon.integration.parser.TenantLineSqlParser;
import net.sf.jsqlparser.expression.*;

/**
 * @author airhead
 */
public interface TenantLineHandler {

  /**
   * 获取租户id参数
   *
   * @return
   */
  Expression getTenantId();

  /**
   * 获取租户id字段
   *
   * @return
   */
  default String getTenantIdColumn() {
    return "tenant_id";
  }

  /**
   * 是否忽略租户
   *
   * @return
   */
  default boolean ignoreTenant() {
    return false;
  }

  /**
   * 是否忽略表格
   *
   * @param tableName
   * @return
   */
  default boolean ignoreTable(String tableName) {
    return false;
  }

  /**
   * 获取SQL 解析器
   *
   * @return
   */
  default SqlParser getSqlParser() {
    return new TenantLineSqlParser(this);
  }

  /**
   * 解析SQL
   *
   * @param sql
   * @param obj
   * @return
   */
  default String parseSql(String sql, Object obj) {
    return getSqlParser().parserSingle(sql, obj);
  }
}

SqlParser

package com.easy.query.solon.integration.parser;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;

/**
 * @author airhead
 */
public interface SqlParser {

  /**
   * 解析单条 SQL
   *
   * @param sql
   * @param obj
   * @return
   */
  default String parserSingle(String sql, Object obj) {
    try {
      Statement statement = CCJSqlParserUtil.parse(sql);
      return this.processParser(statement, 0, sql, obj);
    } catch (JSQLParserException e) {
      throw new RuntimeException(
          String.format("Failed to process, Error SQL: %s", sql), e.getCause());
    }
  }

  /**
   * 解析多条 SQL
   *
   * @param sql
   * @param obj
   * @return
   */
  @SuppressWarnings("deprecation")
  default String parserMulti(String sql, Object obj) {
    try {
      StringBuilder sb = new StringBuilder();
      Statements statements = CCJSqlParserUtil.parseStatements(sql);
      int i = 0;

      for (Statement statement : statements.getStatements()) {
        if (i > 0) {
          sb.append(";");
        }

        sb.append(this.processParser(statement, i, sql, obj));
        ++i;
      }

      return sb.toString();
    } catch (JSQLParserException e) {
      throw new RuntimeException(
          String.format("Failed to process, Error SQL: %s", sql), e.getCause());
    }
  }

  default String processParser(Statement statement, int index, String sql, Object obj) {
    if (statement instanceof Insert) {
      this.processInsert((Insert) statement, index, sql, obj);
    } else if (statement instanceof Select) {
      this.processSelect((Select) statement, index, sql, obj);
    } else if (statement instanceof Update) {
      this.processUpdate((Update) statement, index, sql, obj);
    } else if (statement instanceof Delete) {
      this.processDelete((Delete) statement, index, sql, obj);
    }

    sql = statement.toString();

    return sql;
  }

  default void processInsert(Insert insert, int index, String sql, Object obj) {
    throw new UnsupportedOperationException();
  }

  default void processDelete(Delete delete, int index, String sql, Object obj) {
    throw new UnsupportedOperationException();
  }

  default void processUpdate(Update update, int index, String sql, Object obj) {
    throw new UnsupportedOperationException();
  }

  default void processSelect(Select select, int index, String sql, Object obj) {
    throw new UnsupportedOperationException();
  }
}

TenantLineSqlParser

重点处理 select,如果是插入同时是对单表的操作,使用 ORM 的 dsl 进行操作就可以了。

package com.easy.query.solon.integration.parser;

import com.easy.query.solon.integration.excutor.TenantLineHandler;
import java.util.*;
import java.util.stream.Collectors;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.*;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.*;
import org.dromara.hutool.core.collection.CollUtil;

/**
 * @author airhead
 */
public class TenantLineSqlParser implements SqlParser {
  private final TenantLineHandler tenantLineHandler;

  public TenantLineSqlParser(TenantLineHandler tenantLineHandler) {
    this.tenantLineHandler = tenantLineHandler;
  }

  @Override
  public void processSelect(Select select, int index, String sql, Object obj) {
    String whereSegment = (String) obj;
    this.processSelectBody(select.getPlainSelect(), whereSegment);
    List<WithItem<?>> withItemsList = select.getWithItemsList();
    if (CollUtil.isNotEmpty(withItemsList)) {
      withItemsList.forEach(
          (withItem) -> this.processSelectBody(withItem.getSelect(), whereSegment));
    }
  }

  protected void processSelectBody(Select selectBody, final String whereSegment) {
    if (selectBody == null) {
      return;
    }

    if (selectBody instanceof PlainSelect) {
      processPlainSelect((PlainSelect) selectBody, whereSegment);
    } else if (selectBody instanceof ParenthesedSelect parenthesedSelect) {
      processSelectBody(parenthesedSelect.getSelect(), whereSegment);
    } else if (selectBody instanceof SetOperationList operationList) {
      List<Select> selectBodyList = operationList.getSelects();
      if (CollUtil.isNotEmpty(selectBodyList)) {
        selectBodyList.forEach(body -> processSelectBody(body, whereSegment));
      }
    }
  }

  protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) {
    List<SelectItem<?>> selectItems = plainSelect.getSelectItems();
    if (CollUtil.isNotEmpty(selectItems)) {
      selectItems.forEach((selectItem) -> this.processSelectItem(selectItem, whereSegment));
    }

    Expression where = plainSelect.getWhere();
    this.processWhereSubSelect(where, whereSegment);
    FromItem fromItem = plainSelect.getFromItem();
    List<Table> list = this.processFromItem(fromItem, whereSegment);
    List<Table> mainTables = new ArrayList<>(list);
    List<Join> joins = plainSelect.getJoins();
    if (CollUtil.isNotEmpty(joins)) {
      mainTables = this.processJoins(mainTables, joins, whereSegment);
    }

    if (CollUtil.isNotEmpty(mainTables)) {
      plainSelect.setWhere(this.builderExpression(where, mainTables, whereSegment));
    }
  }

  @SuppressWarnings({"rawtypes", "unchecked"})
  protected void processWhereSubSelect(Expression where, final String whereSegment) {
    if (where == null) {
      return;
    }
    if (where instanceof FromItem) {
      processOtherFromItem((FromItem) where, whereSegment);
      return;
    }
    if (where.toString().indexOf("SELECT") > 0) {
      // 有子查询
      if (where instanceof BinaryExpression expression) {
        // 比较符号 , and , or , 等等
        processWhereSubSelect(expression.getLeftExpression(), whereSegment);
        processWhereSubSelect(expression.getRightExpression(), whereSegment);
      } else if (where instanceof InExpression expression) {
        // in
        Expression inExpression = expression.getRightExpression();
        if (inExpression instanceof Select) {
          processSelectBody(((Select) inExpression), whereSegment);
        }
      } else if (where instanceof ExistsExpression expression) {
        // exists
        processWhereSubSelect(expression.getRightExpression(), whereSegment);
      } else if (where instanceof NotExpression expression) {
        // not exists
        processWhereSubSelect(expression.getExpression(), whereSegment);
      } else if (where instanceof ParenthesedExpressionList) {
        ParenthesedExpressionList<Expression> expression = (ParenthesedExpressionList) where;
        processWhereSubSelect(expression.get(0), whereSegment);
      }
    }
  }

  @SuppressWarnings("rawtypes")
  protected void processSelectItem(SelectItem selectItem, final String whereSegment) {
    Expression expression = selectItem.getExpression();
    if (expression instanceof Select) {
      processSelectBody(((Select) expression), whereSegment);
    } else if (expression instanceof Function) {
      processFunction((Function) expression, whereSegment);
    } else if (expression instanceof ExistsExpression existsExpression) {
      processSelectBody((Select) existsExpression.getRightExpression(), whereSegment);
    }
  }

  /**
   * 处理函数
   *
   * <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)
   *
   * <p>
   *
   * <p>fixed gitee pulls/141
   *
   * @param function
   */
  protected void processFunction(Function function, final String whereSegment) {
    ExpressionList<?> parameters = function.getParameters();
    if (parameters != null) {
      parameters.forEach(
          expression -> {
            if (expression instanceof Select) {
              processSelectBody(((Select) expression), whereSegment);
            } else if (expression instanceof Function) {
              processFunction((Function) expression, whereSegment);
            } else if (expression instanceof EqualsTo) {
              if (((EqualsTo) expression).getLeftExpression() instanceof Select) {
                processSelectBody(
                    ((Select) ((EqualsTo) expression).getLeftExpression()), whereSegment);
              }
              if (((EqualsTo) expression).getRightExpression() instanceof Select) {
                processSelectBody(
                    ((Select) ((EqualsTo) expression).getRightExpression()), whereSegment);
              }
            }
          });
    }
  }

  protected void processOtherFromItem(FromItem fromItem, final String whereSegment) {
    // 去除括号
    while (fromItem instanceof ParenthesedFromItem) {
      fromItem = ((ParenthesedFromItem) fromItem).getFromItem();
    }

    if (fromItem instanceof ParenthesedSelect) {
      Select subSelect = (Select) fromItem;
      processSelectBody(subSelect, whereSegment);
    }
  }

  /** 处理条件 */
  protected Expression builderExpression(
      Expression currentExpression, List<Table> tables, final String whereSegment) {
    // 没有表需要处理直接返回
    if (CollUtil.isEmpty(tables)) {
      return currentExpression;
    }
    // 构造每张表的条件
    List<Expression> expressions =
        tables.stream()
            .map(item -> buildTableExpression(item, currentExpression, whereSegment))
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

    // 没有表需要处理直接返回
    if (CollUtil.isEmpty(expressions)) {
      return currentExpression;
    }

    // 注入的表达式
    Expression injectExpression = expressions.get(0);
    // 如果有多表,则用 and 连接
    if (expressions.size() > 1) {
      for (int i = 1; i < expressions.size(); i++) {
        injectExpression = new AndExpression(injectExpression, expressions.get(i));
      }
    }

    if (currentExpression == null) {
      return injectExpression;
    }
    if (currentExpression instanceof OrExpression) {
      return new AndExpression(
          new ParenthesedExpressionList<>(currentExpression), injectExpression);
    } else {
      return new AndExpression(currentExpression, injectExpression);
    }
  }

  /**
   * 租户字段别名设置
   *
   * <p>tenantId 或 tableAlias.tenantId
   *
   * @param table 表对象
   * @return 字段
   */
  protected Column getAliasColumn(Table table) {
    StringBuilder column = new StringBuilder();
    // todo 该起别名就要起别名,禁止修改此处逻辑
    if (table.getAlias() != null) {
      column.append(table.getAlias().getName()).append(".");
    }
    column.append(tenantLineHandler.getTenantIdColumn());
    return new Column(column.toString());
  }

  /**
   * 构建租户条件表达式
   *
   * @param table 表对象
   * @param where 当前where条件
   * @param whereSegment 所属Mapper对象全路径(在原租户拦截器功能中,这个参数并不需要参与相关判断)
   * @return 租户条件表达式
   */
  protected Expression buildTableExpression(
      final Table table, final Expression where, final String whereSegment) {
    if (tenantLineHandler.ignoreTable(table.getName())) {
      return null;
    }

    return new EqualsTo(getAliasColumn(table), tenantLineHandler.getTenantId());
  }

  protected List<Table> processFromItem(FromItem fromItem, final String whereSegment) {
    // 处理括号括起来的表达式
    //        while (fromItem instanceof ParenthesedFromItem) {
    //            fromItem = ((ParenthesedFromItem) fromItem).getFromItem();
    //        }

    List<Table> mainTables = new ArrayList<>();
    // 无 join 时的处理逻辑
    if (fromItem instanceof Table) {
      Table fromTable = (Table) fromItem;
      mainTables.add(fromTable);
    } else if (fromItem instanceof ParenthesedFromItem) {
      // SubJoin 类型则还需要添加上 where 条件
      List<Table> tables = processSubJoin((ParenthesedFromItem) fromItem, whereSegment);
      mainTables.addAll(tables);
    } else {
      // 处理下 fromItem
      processOtherFromItem(fromItem, whereSegment);
    }
    return mainTables;
  }

  /**
   * 处理 sub join
   *
   * @param subJoin subJoin
   * @return Table subJoin 中的主表
   */
  protected List<Table> processSubJoin(ParenthesedFromItem subJoin, final String whereSegment) {
    while (subJoin.getJoins() == null && subJoin.getFromItem() instanceof ParenthesedFromItem) {
      subJoin = (ParenthesedFromItem) subJoin.getFromItem();
    }
    List<Table> tableList = processFromItem(subJoin.getFromItem(), whereSegment);
    List<Table> mainTables = new ArrayList<>(tableList);
    if (subJoin.getJoins() != null) {
      processJoins(mainTables, subJoin.getJoins(), whereSegment);
    }
    return mainTables;
  }

  /**
   * 处理 joins
   *
   * @param mainTables 可以为 null
   * @param joins      join 集合
   * @return List<Table> 右连接查询的 Table 列表
   */
  protected List<Table> processJoins(
      List<Table> mainTables, List<Join> joins, final String whereSegment) {
    // join 表达式中最终的主表
    Table mainTable = null;
    // 当前 join 的左表
    Table leftTable = null;

    if (mainTables.size() == 1) {
      mainTable = mainTables.get(0);
      leftTable = mainTable;
    }

    // 对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
    Deque<List<Table>> onTableDeque = new LinkedList<>();
    for (Join join : joins) {
      // 处理 on 表达式
      FromItem joinItem = join.getRightItem();

      // 获取当前 join 的表,subJoint 可以看作是一张表
      List<Table> joinTables = null;
      if (joinItem instanceof Table) {
        joinTables = new ArrayList<>();
        joinTables.add((Table) joinItem);
      } else if (joinItem instanceof ParenthesedFromItem) {
        joinTables = processSubJoin((ParenthesedFromItem) joinItem, whereSegment);
      }

      if (joinTables != null && !joinTables.isEmpty()) {

        // 如果是隐式内连接
        if (join.isSimple()) {
          mainTables.addAll(joinTables);
          continue;
        }

        // 当前表是否忽略
        Table joinTable = joinTables.get(0);

        List<Table> onTables = null;
        // 如果不要忽略,且是右连接,则记录下当前表
        if (join.isRight()) {
          mainTable = joinTable;
          mainTables.clear();
          if (leftTable != null) {
            onTables = Collections.singletonList(leftTable);
          }
        } else if (join.isInner()) {
          if (mainTable == null) {
            onTables = Collections.singletonList(joinTable);
          } else {
            onTables = Arrays.asList(mainTable, joinTable);
          }
          mainTable = null;
          mainTables.clear();
        } else {
          onTables = Collections.singletonList(joinTable);
        }

        if (mainTable != null && !mainTables.contains(mainTable)) {
          mainTables.add(mainTable);
        }

        // 获取 join 尾缀的 on 表达式列表
        Collection<Expression> originOnExpressions = join.getOnExpressions();
        // 正常 join on 表达式只有一个,立刻处理
        if (originOnExpressions.size() == 1 && onTables != null) {
          List<Expression> onExpressions = new LinkedList<>();
          onExpressions.add(
              builderExpression(originOnExpressions.iterator().next(), onTables, whereSegment));
          join.setOnExpressions(onExpressions);
          leftTable = mainTable == null ? joinTable : mainTable;
          continue;
        }
        // 表名压栈,忽略的表压入 null,以便后续不处理
        onTableDeque.push(onTables);
        // 尾缀多个 on 表达式的时候统一处理
        if (originOnExpressions.size() > 1) {
          Collection<Expression> onExpressions = new LinkedList<>();
          for (Expression originOnExpression : originOnExpressions) {
            List<Table> currentTableList = onTableDeque.poll();
            if (CollUtil.isEmpty(currentTableList)) {
              onExpressions.add(originOnExpression);
            } else {
              onExpressions.add(
                  builderExpression(originOnExpression, currentTableList, whereSegment));
            }
          }
          join.setOnExpressions(onExpressions);
        }
        leftTable = joinTable;
      } else {
        processOtherFromItem(joinItem, whereSegment);
        leftTable = null;
      }
    }

    return mainTables;
  }
}

DbManager

在DbManager 中替换默认的 EntityExpressionExecutor。

easyQueryBuilderConfiguration
    .replaceService(DataSourceUnitFactory.class, SolonDataSourceUnitFactory.class)
    .replaceService(ConnectionManager.class, SolonConnectionManager.class)
    .replaceService(appContext)
    .replaceService(EntityExpressionExecutor.class, DefaultEntityExpressionExecutorEx.class);

使用租户

使用时只需要继承 TenantLineHandler,设置对应的租户字段的处理,构建为Bean 对象就可以了。

结构裁剪

虽然 solon 可以设置数据为 null 时不返回属性,但这个和返回结果的裁剪还是有不同。如果通过 easy-query 来实现的话,就需要增加对的 Vo,或者 Dto 来承接数据。这里提供对 Model 进行封装的方式实现对返回结果结构的裁剪。

BaseModel

通过一个baseModel 看和普通model的不同。

package com.goldsyear.porpoise.module.dev.code.model.base;

import com.easy.query.core.annotation.Column;
import com.easy.query.solon.activerecord.IBean;
import com.easy.query.solon.activerecord.Model;
import java.time.LocalDateTime;

/**
 * 1. private 变量在 Getter 和 Setter 中没有被用到。之所以定义它们,是因为很多序列化框架的需要 private 变量和 Setter 必须同时存在才能成功。 2.
 * 方便设置 easy-query 的注解
 *
 * <p>Generated by Porpoise, please do not modify this file.
 *
 * @author airhead
 */
@SuppressWarnings({"unused"})
public abstract class BaseCodeDb<M extends BaseCodeDb<M>> extends Model<M> implements IBean {

  /** 主键编号 */
  @Column(primaryKey = true, generatedKey = true)
  private Integer id;

  /** 名称 */
  private String name;

  /** 连接 */
  private String url;

  /** 用户名 */
  private String username;

  /** 密码 */
  private String password;

  /** 创建者 */
  private String creator;

  /** 创建时间 */
  private LocalDateTime createTime;

  /** 更新者 */
  private String updater;

  /** 更新时间 */
  private LocalDateTime updateTime;

  /** 主键编号 */
  public Integer getId() {
    return getInt("id");
  }

  /** 主键编号 */
  public void setId(Integer id) {
    set("id", id);
  }

  /** 名称 */
  public String getName() {
    return getStr("name");
  }

  /** 名称 */
  public void setName(String name) {
    set("name", name);
  }

  /** 连接 */
  public String getUrl() {
    return getStr("url");
  }

  /** 连接 */
  public void setUrl(String url) {
    set("url", url);
  }

  /** 用户名 */
  public String getUsername() {
    return getStr("username");
  }

  /** 用户名 */
  public void setUsername(String username) {
    set("username", username);
  }

  /** 密码 */
  public String getPassword() {
    return getStr("password");
  }

  /** 密码 */
  public void setPassword(String password) {
    set("password", password);
  }

  /** 创建者 */
  public String getCreator() {
    return getStr("creator");
  }

  /** 创建者 */
  public void setCreator(String creator) {
    set("creator", creator);
  }

  /** 创建时间 */
  public LocalDateTime getCreateTime() {
    return getLocalDateTime("create_time");
  }

  /** 创建时间 */
  public void setCreateTime(LocalDateTime createTime) {
    set("create_time", createTime);
  }

  /** 更新者 */
  public String getUpdater() {
    return getStr("updater");
  }

  /** 更新者 */
  public void setUpdater(String updater) {
    set("updater", updater);
  }

  /** 更新时间 */
  public LocalDateTime getUpdateTime() {
    return getLocalDateTime("update_time");
  }

  /** 更新时间 */
  public void setUpdateTime(LocalDateTime updateTime) {
    set("update_time", updateTime);
  }
}

ModelUtils

通过 ModelUtils,可以对查询结果进行裁剪。

package com.goldsyear.solon.web.util;

import com.easy.query.solon.activerecord.Model;
import com.goldsyear.solon.web.model.PageResult;
import java.util.List;
import org.dromara.hutool.core.collection.CollUtil;

/**
 * @author airhead
 */
public class ModelUtils {
  public static <M extends Model<M>> M remove(M model, String attr) {
    if (model == null) {
      return null;
    }

    model.remove(attr);
    return model;
  }

  public static <M extends Model<M>> List<M> remove(List<M> modelList, String attr) {
    if (CollUtil.isEmpty(modelList)) {
      return modelList;
    }

    for (Model<M> model : modelList) {
      model.remove(attr);
    }

    return modelList;
  }

  public static <M extends Model<M>> PageResult<M> remove(PageResult<M> pageResult, String attr) {
    if (pageResult == null) {
      return pageResult;
    }

    remove(pageResult.getRecords(), attr);
    return pageResult;
  }

  public static <M extends Model<M>> M remove(M model, String... attrs) {
    if (model == null) {
      return model;
    }

    model.remove(attrs);
    return model;
  }

  public static <M extends Model<M>> List<M> remove(List<M> modelList, String... attrs) {
    if (CollUtil.isEmpty(modelList)) {
      return modelList;
    }

    for (Model<M> model : modelList) {
      model.remove(attrs);
    }

    return modelList;
  }

  public static <M extends Model<M>> PageResult<M> remove(
      PageResult<M> pageResult, String... attrs) {
    if (pageResult == null) {
      return pageResult;
    }

    remove(pageResult.getRecords(), attrs);

    return pageResult;
  }

  public static <M extends Model<M>> M keep(M model, String attr) {
    if (model == null) {
      return model;
    }

    model.keep(attr);
    return model;
  }

  public static <M extends Model<M>> List<M> keep(List<M> modelList, String attr) {
    if (CollUtil.isEmpty(modelList)) {
      return modelList;
    }

    for (Model<M> model : modelList) {
      model.keep(attr);
    }

    return modelList;
  }

  public static <M extends Model<M>> PageResult<M> keep(PageResult<M> pageResult, String attr) {
    if (pageResult == null) {
      return pageResult;
    }

    keep(pageResult.getRecords(), attr);

    return pageResult;
  }

  public static <M extends Model<M>> M keep(M model, String... attrs) {
    if (model == null) {
      return model;
    }

    model.keep(attrs);

    return model;
  }

  public static <M extends Model<M>> List<M> keep(List<M> modelList, String... attrs) {
    if (CollUtil.isEmpty(modelList)) {
      return modelList;
    }

    for (Model<M> model : modelList) {
      model.keep(attrs);
    }

    return modelList;
  }

  public static <M extends Model<M>> PageResult<M> keep(PageResult<M> pageResult, String... attrs) {
    if (pageResult == null) {
      return pageResult;
    }

    keep(pageResult.getRecords(), attrs);

    return pageResult;
  }
}

AOP

Deform
package com.goldsyear.solon.web.annotation;

import java.lang.annotation.*;

/**
 * @author airhead
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Deform {
  /**
   * 保留的属性
   *
   * @return String[]
   */
  String[] keep() default {};

  /**
   * 移除的属性
   *
   * @return String[]
   */
  String[] remove() default {};
}
DeformInterceptor
package com.goldsyear.solon.web.interceptor;

import com.easy.query.solon.activerecord.Model;
import com.goldsyear.solon.web.annotation.Deform;
import com.goldsyear.solon.web.model.PageResult;
import com.goldsyear.solon.web.util.ModelUtils;
import java.util.Arrays;
import java.util.List;
import org.dromara.hutool.core.collection.CollUtil;
import org.noear.solon.core.aspect.Interceptor;
import org.noear.solon.core.aspect.Invocation;

/**
 * @author airhead
 */
public class DeformInterceptor implements Interceptor {
  @SuppressWarnings({"rawtypes", "unchecked"})
  @Override
  public Object doIntercept(Invocation inv) throws Throwable {
    Object ret = inv.invoke();

    Deform deform = inv.getMethodAnnotation(Deform.class);
    // 不需要转换
    if (deform == null) {
      return ret;
    }

    if (ret instanceof Model) {
      if (CollUtil.isNotEmpty(Arrays.asList(deform.keep()))) {
        Model model = (Model) ret;
        return ModelUtils.keep(model, deform.keep());
      }

      if (CollUtil.isNotEmpty(Arrays.asList(deform.remove()))) {
        Model model = (Model) ret;
        return ModelUtils.keep(model, deform.keep());
      }
    }

    if (ret instanceof PageResult) {
      if (CollUtil.isNotEmpty(Arrays.asList(deform.keep()))) {
        PageResult pageResult = (PageResult) ret;
        return ModelUtils.keep(pageResult.getRecords(), deform.keep());
      }

      if (CollUtil.isNotEmpty(Arrays.asList(deform.remove()))) {
        PageResult pageResult = (PageResult) ret;
        return ModelUtils.remove(pageResult.getRecords(), deform.keep());
      }
    }

    if (ret instanceof List) {
      if (CollUtil.isNotEmpty(Arrays.asList(deform.keep()))) {
        List list = (List) ret;
        return ModelUtils.keep(list, deform.keep());
      }

      if (CollUtil.isNotEmpty(Arrays.asList(deform.remove()))) {
        List list = (List) ret;
        return ModelUtils.remove(list, deform.keep());
      }
    }

    return ret;
  }
}
注册 DeformInterceptor
   // 
    context.beanInterceptorAdd(Deform.class, new DeformInterceptor());

使用

注意这里的是字段名,而不是类的属性名。

  @Deform(keep = {"id", "name"})
  public List<CodeColumn> simpleListCodeColumn(CodeColumn codeColumn) {
    List<CodeColumn> list = list(codeColumn);
//    ModelUtils.keep(list, "id", "name");
    return list;
  }

序列化相关

因为 Model 和 Activerecord,渲染的时候并不是直接渲染成员,而是对应的 attr 成员下的内容,因此需要特殊处理。这里使用了 snack3, 如果使用了其他的json 框架,需要根据实际情况进行处理。

package com.goldsyear.solon.web;

import com.easy.query.solon.activerecord.Model;
import com.easy.query.solon.activerecord.Record;
import com.goldsyear.solon.web.annotation.Deform;
import com.goldsyear.solon.web.interceptor.DeformInterceptor;
import com.goldsyear.solon.web.model.ModelEncoder;
import com.goldsyear.solon.web.model.ModelFieldGetter;
import com.goldsyear.solon.web.model.RecordEncoder;
import com.goldsyear.solon.web.model.RecordFieldGetter;
import com.jfinal.kit.PathKit;
import com.jfinal.template.Engine;
import java.time.LocalDateTime;
import org.dromara.hutool.core.date.TimeUtil;
import org.noear.snack.core.Feature;
import org.noear.snack.core.Options;
import org.noear.solon.core.AppContext;
import org.noear.solon.core.Plugin;
import org.noear.solon.serialization.snack3.SnackActionExecutor;
import org.noear.solon.serialization.snack3.SnackRenderFactory;
import org.noear.solon.web.staticfiles.StaticMappings;
import org.noear.solon.web.staticfiles.repository.FileStaticRepository;

/**
 * @author airhead
 */
public class EasyQueryWebPluginImp implements Plugin {
  static boolean started = false;

  @Override
  public void start(AppContext context) {
    if (started) {
      return;
    }

    StaticMappings.add("/", new FileStaticRepository(PathKit.getWebRootPath()));

    Engine.addFieldGetterToLast(new ModelFieldGetter());
    Engine.addFieldGetterToLast(new RecordFieldGetter());
    
    // 处理序列化
    context.getBeanAsync(
        SnackRenderFactory.class,
        factory -> {
          factory.addConvertor(LocalDateTime.class, TimeUtil::formatNormal);
          factory.addEncoder(Model.class, new ModelEncoder<>());
          factory.addEncoder(Record.class, new RecordEncoder<>());
        });

    // 处理反序列化,通过setter来设值。
      context.getBeanAsync(
        SnackActionExecutor.class,
        snackActionExecutor -> {
          Options config = snackActionExecutor.config();
          config.add(Feature.UseSetter, Feature.DisableClassNameRead);
        });

    // 注册 DeformInterceptor
    context.beanInterceptorAdd(Deform.class, new DeformInterceptor());

    started = true;
  }
}

Base Encoder

package com.goldsyear.solon.web.model;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.noear.snack.ONode;
import org.noear.solon.Solon;
import org.noear.solon.core.Props;

/**
 * @author airhead
 */
public class BaseEncoder {
  /** 对 Model 和 Record 的字段名进行转换的函数。例如转成驼峰形式对 oracle 支持更友好 */
  protected static Function<String, String> fieldNameConverter = StrUtil::toCamelCase;

  protected Props props = Solon.cfg().getProp("solon.serialization.json");

  protected void encode(ONode node, Map<String, Object> map) {
    if (CollUtil.isEmpty(map)) {
      return;
    }

    Map<String, Object> newMap = new HashMap<>(map.size());
    for (Map.Entry<String, Object> entry : map.entrySet()) {
      String fieldName = entry.getKey();
      Object value = entry.getValue();
      String attrName = fieldName;
      if (fieldNameConverter != null) {
        attrName = fieldNameConverter.apply(fieldName);
      }
      newMap.put(attrName, value);
    }

    node.fill(newMap);
  }
}

ModelEncoder

package com.goldsyear.solon.web.model;

import com.easy.query.solon.activerecord.CPI;
import com.easy.query.solon.activerecord.Model;
import org.noear.snack.ONode;
import org.noear.snack.core.NodeEncoder;

/**
 * @author airhead
 */
public class ModelEncoder<T extends Model<?>> extends BaseEncoder implements NodeEncoder<T> {
  @Override
  public void encode(T data, ONode node) {
    if (data == null) {
      return;
    }

    encode(node, CPI.getAttrs(data));
  }
}

RecordEncorder

package com.goldsyear.solon.web.model;

import com.easy.query.solon.activerecord.Record;
import org.noear.snack.ONode;
import org.noear.snack.core.NodeEncoder;

/**
 * @author airhead
 */
public class RecordEncoder<T extends Record> extends BaseEncoder implements NodeEncoder<T> {

  @Override
  public void encode(T data, ONode node) {
    if (data == null) {
      return;
    }

    encode(node, data.getColumns());
  }
}

获取 Model

protected <T> T getBean(Class<T> type) {
  T t = this.context().paramAsBean(type);
  if (Model.class.isAssignableFrom(type)) {
    if (t != null) {
      Model<?> model = (Model<?>) t;
      for (KeyValues<String> values : context().paramMap()) {
        // 此处通过Map的方式设置
        model.put(NamingCase.toUnderlineCase(values.getKey()), values.getFirstValue());
      }

      return t;
    }
  }

  return t;
}

protected <T> T getModel(Class<T> type) {
  return getModel(type, StrKit.firstCharToLowerCase(type.getSimpleName()));
}

protected <T> T getModel(Class<T> type, String name) {
  MultiMap<String> paramMap = context().paramMap();
  return PropsConverter.global().convert(new Props().addAll(paramMap).getProp(name), type);
}

小结

通过以上的扩展,easy-query 已经可以使用 ActiveRecord Model 的功能,可以用 SQL 模版的方式管理 SQL,并增强了ActiveRecord 的 SQL 能力,支持租户字段的过滤。同时借助 Solon 的 AOP 的能力,通过一个注解实现对结果结构的裁剪,减少转换类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值