说明
在「 操作 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 的能力,通过一个注解实现对结果结构的裁剪,减少转换类。