一. 概述
之前一篇了解 JDBC 层之 QueryDSL简单介绍了有关 QueryDSL 的大体的功能,拼装 SQL 和结果映射;这篇将介绍其拼装 SQL 原理以及结果映射;同时也会介绍高级配置;
有关 SQL 生成的采用观察者模式,如果没有这模式不是太清楚的,可以去查阅刘伟大神的四篇访问者模式介绍,其主要很好的利用方法重载这么一个特性来实现该模式。这里不在这儿过多讲解其模式。
二. SQL 生成
2.1 数据结构
我们重点看一些 DefaultQueryMetadata 对象数据结构,这个对象在生成 SQL(SQLSerializer 来生成 SQL)过程,会根据其来相关的属性值来做追加相应的 SQL 内容;
2.2 Expression
上面的两小节,属性值的类型以及方法中都涉及一个关键接口 Expression。如果将其实现类进行完全介绍出来,难度会很大;我这里只把关键的内容介绍,会对其进行有了大概的认识;
2.2.1 Path
该接口是路径表达式,在 SQL 中可以理解为元数据表的表明以及字段名;
该接口的主要的实现类图如下:
其 Path 会根据不同的类型来对应的不同类型的 Path 实现类;这里只列了一些类;
其中 RelationPathBase 是元数据表的数据结构,其会包含表的一些元数据信息,如字段名、索引等信息;
我们通过查看 TimePath、StringPath 等类,都包含了 PathImpl 属性值,所以可以这么认为这些类型的 Path 都是一个适配器,真正存放的内容的是 PathImpl;假设产品表下有商品名称这个字段,那么查看其 PathMetadata 结构如图所示:
2.2.2 FactoryExpression
工厂表达式,用于封装 Select 中 Fields_names 也就是 Path 集合,同时会携带映射结果功能;
这里我们常用的是 QBean 以及 QTuple 这两个对象;
我们可以通过 Projections 这个工具类来创建 FactoryExpression 对应的实现类;
2.2.3 Operation
操作符表达式,例如运算符等都会封装这个接口对应的实现类;我们一般都不会直接创建对应的实现类,而是由 Path 实现类(字段对象)的提供的方法去创建对应的 Operation 对象;
例如订单表中有这么一个价格(数值类型),我们汇总满足条件的价格,那么我们可以调用价格对象 sum 方法,那么(NumberPath),代码如下:
public NumberExpression<T> sum() { if (sum == null) { //其是创建NumberOperation这么一个对象; sum = Expressions.numberOperation(getType(), Ops.AggOps.SUM_AGG, mixin); } return sum;}
复制代码
该接口的跟 Path 有点类似,很多实现类都是一个空壳,关键的存放内容的是在 OperationImpl;该对象就存放了两个属性值:
2.2.4 Constant
常量表达式,比较好理解,可以这么理解:SQL 中“?”占位符要填充的值是我们要注入的值,该值一般用这个对象来包装;
2.2.5 SubQueryExpression
子查询表达式,其实现类是 SubQueryExpressionImpl,其内部的属性是 QueryMetadata 接口(其实现类 DefaultQueryMetadata).
2.2.6 TemplateExpression
模板表达式,这里目前没有发现在 query-sql 工程中没有使用到这个,姑且不再介绍;我们主要看其主要的实现类 TemplateExpressionImpl 中包含的属性:
2.2.7 ParamExpression
参数表达式,目前在 query-sql 工程中,并没有使用到这个。这个不再介绍。只介绍其主要的实现类 ParamExpressionImpl 中包含的属性:
2.2.8 DslExpression
DSL 表达式,很多上面介绍的实现类很多都继承了该接口下的子类,用于操作生产对应的目标对象;就如上面介绍的例子 NumberPath,调用 sum 方法,就是继承 DslExpression 接口下的 NumberExpression 子类。
2.2.9 Predicate
条件表达式。我们并不会直接去创建该对象,而是通过字段对象中的方法去创建,例如 StringPath 对象"等于"方法:
public BooleanExpression eq(Expression<? super T> right) { return Expressions.booleanOperation(Ops.EQ, mixin, right);}
复制代码
其创建的对象是一个适配类对象 BooleanOperation,其内部是使用 OperationImpl 来保存相关的数据;
2.2.10 其他
目前在 SQL 中并没有使用到这个,不介绍
2.3 Visitor
根据观察者模式,在 QueryDSL 提供了这么一个统一接口:
该接口有很多实现类,这里我只列出关键的实现类;
我们重点介绍 SQLSerializer 对象的关键代码。
2.3.1 Constant
@Overridepublic final Void visit(Constant<?> expr, Void context) { //获取常量值 visitConstant(expr.getConstant()); return null;}@Overridepublic void visitConstant(Object constant) { if (useLiterals) {//默认该值是为false //使用字面量去访问,也就是往SQL中追加对应的内容,而不是采用?占位符去设置; //为了安全着想,基本不会设置为true if (constant instanceof Collection) { append("("); boolean first = true; for (Object o : ((Collection) constant)) { if (!first) { append(COMMA); } append(configuration.asLiteral(o)); first = false; } append(")"); } else { append(configuration.asLiteral(constant)); } } else if (constant instanceof Collection) { //其追加内容格式:(?,?...?) append("("); boolean first = true; for (Object o : ((Collection) constant)) { if (!first) { append(COMMA); } //插入?占位符,接着会将参数值保存到常量集合中, //setParameters中设置该值; append("?"); constants.add(o); if (first && (constantPaths.size() < constants.size())) { constantPaths.add(null); } first = false; } append(")"); int size = ((Collection) constant).size() - 1; Path<?> lastPath = constantPaths.peekLast(); for (int i = 0; i < size; i++) { constantPaths.add(lastPath); } } else { if (stage == Stage.SELECT && !Null.class.isInstance(constant) && configuration.getTemplates().isWrapSelectParameters()) { //在select阶段,且wrapSelectParameters为true时, //会使用模板cast({0} as {1s})去追加; //CAST (expression AS data_type) => CAST函数用于将某种数据类型的表达式显式转换为另一种数据类型 String typeName = configuration.getTypeNameForCast(constant.getClass()); Expression type = Expressions.constant(typeName); super.visitOperation(constant.getClass(), SQLOps.CAST, ImmutableList.<Expression<?>>of(Q, type)); } else { append("?"); } //插入?占位符,接着会将参数值保存到常量集合中, //setParameters中设置该值; constants.add(constant); if (constantPaths.size() < constants.size()) { constantPaths.add(null); } }}
复制代码
2.3.2 FactoryExpression
其操作比较简单,就是获取 FactoryExpression 中的参数,也就是 Path 集合(select 后面要查询的字段集合),最终追加的内容:field_name, field_name......。代码如下:
@Overridepublic Void visit(FactoryExpression<?> expr, Void context) { handle(", ", expr.getArgs()); return null;}public final S handle(final String sep, final List<? extends Expression<?>> expressions) { for (int i = 0; i < expressions.size(); i++) { if (i != 0) { append(sep); } //基本会调用Path的访问方法 handle(expressions.get(i)); } return self;}
复制代码
2.3.3 Path
其追加路径,其有上下级关系,所以基本是要追加:
代码如下:
@Overridepublic Void visit(Path<?> path, Void context) { if (dml) { //只要做update、insert、delete操作时,dml才为true. if (path.equals(entity) && path instanceof RelationalPath<?>) { SchemaAndTable schemaAndTable = getSchemaAndTable((RelationalPath<?>) path); boolean precededByDot; if (dmlWithSchema && templates.isPrintSchema()) { //追加schema. appendSchemaName(schemaAndTable.getSchema()); append("."); precededByDot = true; } else { precededByDot = false; } //追加表名 appendTableName(schemaAndTable.getTable(), precededByDot); return null; } else if (entity.equals(path.getMetadata().getParent()) && skipParent) { //追加字段名 appendAsColumnName(path, false); return null; } } //是select语句的访问; final PathMetadata metadata = path.getMetadata(); boolean precededByDot; if (metadata.getParent() != null && (!skipParent || dml)) { //访问上级,也就是父级的path路径,在常规中会加上表名这个字符串。 visit(metadata.getParent(), context); append("."); precededByDot = true; } else { precededByDot = false; } // appendAsColumnName(path, precededByDot); return null;}protected void appendAsColumnName(Path<?> path, boolean precededByDot) { //获取该路径下的element, String column = ColumnMetadata.getName(path); if (path.getMetadata().getParent() instanceof RelationalPath) { RelationalPath<?> parent = (RelationalPath<?>) path.getMetadata().getParent(); column = configuration.getColumnOverride(parent.getSchemaAndTable(), column); } append(templates.quoteIdentifier(column, precededByDot));}
复制代码
2.3.4 Operation
其是根据操作符(Operator)从 Template 找到对应的模板,然后采用替换的形式操作,接着追加到 SQL 中;代码如下:
@Overridepublic Void visit(Operation<?> expr, Void context) { visitOperation(expr.getType(), expr.getOperator(), expr.getArgs()); return null;}protected void visitOperation(Class<?> type, Operator operator, final List<? extends Expression<?>> args) { //从template模板对象中找到对应的模板 final Template template = templates.getTemplate(operator); if (template != null) { final int precedence = templates.getPrecedence(operator); boolean first = true; for (final Template.Element element : template.getElements()) { //从args中获取对应的对象信息; final Object rv = element.convert(args); if (rv instanceof Expression) { //如果是表达式 final Expression<?> expr = (Expression<?>) rv; if (precedence > -1 && expr instanceof Operation) { Operator op = ((Operation<?>) expr).getOperator(); int opPrecedence = templates.getPrecedence(op); if (precedence < opPrecedence) { append("(").handle(expr).append(")"); } else if (!first && precedence == opPrecedence && !SAME_PRECEDENCE.contains(op)) { append("(").handle(expr).append(")"); } else { handle(expr); } } else { handle(expr); } first = false; } else if (element.isString()) { //如果是字符串类型的,则追加字符串值 append(rv.toString()); } else { //其他的,则认为是常量 visitConstant(rv); } } } else if (strict) { throw new IllegalArgumentException("No pattern found for " + operator); } else { append(operator.toString()); append("("); handle(", ", args); append(")"); }}
复制代码
2.3.5 SubQueryExpression
则重复上面的操作而已,这里不在阐述;
2.3.6 其他
这里不在阐述了,具体可以查看其代码即可;而且不是我们使用该框架会很少被调用,所以不再简述;
2.4 生成 SQL
所有的入口在 SqlSerializer 对象中;针对不同的操作,会有对应的方法提供;
2.4.1 生成 Query
其入口是 serializeForQuery,这里只在代码阐述关键的代码处进行解释,代码如下:
protected void serializeForQuery(QueryMetadata metadata, boolean forCountRow) { boolean oldInSubquery = inSubquery; inSubquery = inSubquery || getLength() > 0; boolean oldSkipParent = skipParent; skipParent = false; //从DefaultQueryMetadata对象sql对应的部分值,有关DefaultQueryMetadata的介绍,在数据结构有介绍。 final Expression<?> select = metadata.getProjection(); final List<JoinExpression> joins = metadata.getJoins(); final Predicate where = metadata.getWhere(); final List<? extends Expression<?>> groupBy = metadata.getGroupBy(); final Predicate having = metadata.getHaving(); final List<OrderSpecifier<?>> orderBy = metadata.getOrderBy(); final Set<QueryFlag> flags = metadata.getFlags(); final boolean hasFlags = !flags.isEmpty(); String suffix = null; List<? extends Expression<?>> sqlSelect; if (select instanceof FactoryExpression) { sqlSelect = ((FactoryExpression<?>) select).getArgs(); } else if (select != null) { sqlSelect = ImmutableList.of(select); } else { sqlSelect = ImmutableList.of(); } //在with阶段,如果有with操作,则追加with 表达式 if (hasFlags) { List<Expression<?>> withFlags = Lists.newArrayList(); boolean recursive = false; for (QueryFlag flag : flags) { if (flag.getPosition() == Position.WITH) { if (flag.getFlag() == SQLTemplates.RECURSIVE) { recursive = true; continue; } withFlags.add(flag.getFlag()); } } if (!withFlags.isEmpty()) { if (recursive) { append(templates.getWithRecursive()); } else { append(templates.getWith()); } handle(", ", withFlags); append("\n"); } } // 在start阶段 if (hasFlags) { serialize(Position.START, flags); } // 在select阶段 Stage oldStage = stage; stage = Stage.SELECT; if (forCountRow) { //如果是查询总数的,即count的,select append(templates.getSelect()); if (hasFlags) { serialize(Position.AFTER_SELECT, flags); } if (!metadata.isDistinct()) { //追加count(*) from (select 1 as one ..... ) internal append(templates.getCountStar()); if (!groupBy.isEmpty()) { append(templates.getFrom()); append("("); append(templates.getSelect()); append("1 as one "); suffix = ") internal"; } } else { List<? extends Expression<?>> columns; if (sqlSelect.isEmpty()) { columns = getIdentifierColumns(joins, !templates.isCountDistinctMultipleColumns()); } else { columns = sqlSelect; } if (!groupBy.isEmpty()) { // select count(*) from (select distinct ...) append(templates.getCountStar()); append(templates.getFrom()); append("("); append(templates.getSelectDistinct()); handleSelect(COMMA, columns); suffix = ") internal"; } else if (columns.size() == 1) { append(templates.getDistinctCountStart()); handle(columns.get(0)); append(templates.getDistinctCountEnd()); } else if (templates.isCountDistinctMultipleColumns()) { append(templates.getDistinctCountStart()); append("(").handleSelect(COMMA, columns).append(")"); append(templates.getDistinctCountEnd()); } else { // select count(*) from (select distinct ...) append(templates.getCountStar()); append(templates.getFrom()); append("("); append(templates.getSelectDistinct()); handleSelect(COMMA, columns); suffix = ") internal"; } } } else if (!sqlSelect.isEmpty()) { //在select阶段,追加字段名信息 if (!metadata.isDistinct()) { append(templates.getSelect()); } else { append(templates.getSelectDistinct()); } if (hasFlags) { serialize(Position.AFTER_SELECT, flags); } handleSelect(COMMA, sqlSelect); } if (hasFlags) { serialize(Position.AFTER_PROJECTION, flags); } // 在from阶段,追加表以及表连接表达式 stage = Stage.FROM; serializeSources(joins); // 在where阶段,追加条件表达式 if (hasFlags) { serialize(Position.BEFORE_FILTERS, flags); } if (where != null) { stage = Stage.WHERE; append(templates.getWhere()).handle(where); } if (hasFlags) { serialize(Position.AFTER_FILTERS, flags); } // 在group by阶段,追加groupby表达式 if (hasFlags) { serialize(Position.BEFORE_GROUP_BY, flags); } if (!groupBy.isEmpty()) { stage = Stage.GROUP_BY; append(templates.getGroupBy()).handle(COMMA, groupBy); } if (hasFlags) { serialize(Position.AFTER_GROUP_BY, flags); } // 在having阶段,追加having表达式 if (hasFlags) { serialize(Position.BEFORE_HAVING, flags); } if (having != null) { stage = Stage.HAVING; append(templates.getHaving()).handle(having); } if (hasFlags) { serialize(Position.AFTER_HAVING, flags); } // 在order by阶段,追加orderby表达式 if (hasFlags) { serialize(Position.BEFORE_ORDER, flags); } if (!orderBy.isEmpty() && !forCountRow) { stage = Stage.ORDER_BY; append(templates.getOrderBy()); handleOrderBy(orderBy); } if (hasFlags) { serialize(Position.AFTER_ORDER, flags); } // 在modifiers阶段,追加分页表达式 if (!forCountRow && metadata.getModifiers().isRestricting() && !joins.isEmpty()) { stage = Stage.MODIFIERS; templates.serializeModifiers(metadata, this); } if (suffix != null) { append(suffix); } // reset stage stage = oldStage; skipParent = oldSkipParent; inSubquery = oldInSubquery;}
复制代码
2.4.2 生成 Update
其入口是 serializeForUpdate,代码如下:
/** * entity是目标表, update是更新目标表的指定的字段以及对应的值集合 */protected void serializeForUpdate(QueryMetadata metadata, RelationalPath<?> entity, Map<Path<?>, Expression<?>> updates) { this.entity = entity; //在start阶段,追加update 表名 serialize(Position.START, metadata.getFlags()); if (!serialize(Position.START_OVERRIDE, metadata.getFlags())) { append(templates.getUpdate()); } serialize(Position.AFTER_SELECT, metadata.getFlags()); boolean originalDmlWithSchema = dmlWithSchema; dmlWithSchema = true; handle(entity); dmlWithSchema = originalDmlWithSchema; append("\n"); append(templates.getSet()); boolean first = true; skipParent = true; //追加 set field = field_value for (final Map.Entry<Path<?>,Expression<?>> update : updates.entrySet()) { if (!first) { append(COMMA); } handle(update.getKey()); append(" = "); if (!useLiterals && update.getValue() instanceof Constant<?>) { constantPaths.add(update.getKey()); } handle(update.getValue()); first = false; } skipParent = false; //追加where条件 if (metadata.getWhere() != null) { serializeForWhere(metadata); }}
复制代码
2.4.3 生成 Insert
其入口是 serializeForInsert,这里有分单个也有批量插入的,这里只介绍单个插入的代码:
/** * columns是要插入指定字段的集合 * values 指定字段对应的值得集合 * subQuery子查询 */protected void serializeForInsert(QueryMetadata metadata, RelationalPath<?> entity, List<Path<?>> columns, List<Expression<?>> values, @Nullable SubQueryExpression<?> subQuery) { serialize(Position.START, metadata.getFlags()); if (!serialize(Position.START_OVERRIDE, metadata.getFlags())) { //追加insert into append(templates.getInsertInto()); } serialize(Position.AFTER_SELECT, metadata.getFlags()); boolean originalDmlWithSchema = dmlWithSchema; dmlWithSchema = true; handle(entity); dmlWithSchema = originalDmlWithSchema; // 追加要插入的字段(column1,column2...) if (!columns.isEmpty()) { append(" ("); skipParent = true; handle(COMMA, columns); skipParent = false; append(")"); } if (subQuery != null) { //追加select column.... from table where ....语句 append("\n"); serialize(subQuery.getMetadata(), false); } else { if (!useLiterals) { //将值保存到constantPaths for (int i = 0; i < columns.size(); i++) { if (values.get(i) instanceof Constant<?>) { constantPaths.add(columns.get(i)); } } } if (!values.isEmpty()) { // 追加values (?,?....?) append(templates.getValues()); append("("); handle(COMMA, values); append(")"); } else { append(templates.getDefaultValues()); } } }
复制代码
2.4.4 生成 Delete
其入口是 serializeForDelete,代码如下:
protected void serializeForDelete(QueryMetadata metadata, RelationalPath<?> entity) { serialize(Position.START, metadata.getFlags()); //追加delete if (!serialize(Position.START_OVERRIDE, metadata.getFlags())) { append(templates.getDelete()); } serialize(Position.AFTER_SELECT, metadata.getFlags()); //追加from append("from "); boolean originalDmlWithSchema = dmlWithSchema; dmlWithSchema = true; //追加表名 handle(entity); dmlWithSchema = originalDmlWithSchema; //追加where后的条件 if (metadata.getWhere() != null) { serializeForWhere(metadata); }}
复制代码
三. 结果映射
将 sql 执行响应的结果很多情况下,需要将响应报文进行映射,映射有三种。
3.1 基于 FactoryExpression
//AbstractSQLQuery.fetchif (expr instanceof FactoryExpression) { FactoryExpression<T> fe = (FactoryExpression<T>) expr; while (rs.next()) { if (getLastCell) { //如果getLastCell为true时,同时会添加count(*) over() 这样语句, //来做满足条件下的有多少条记录,会放在最后的一列。而且只取一次即可。 lastCell = rs.getObject(fe.getArgs().size() + 1); getLastCell = false; } //这个才是真正的结果映射 rv.add(newInstance(fe, rs, 0)); }}private <RT> RT newInstance(FactoryExpression<RT> c, ResultSet rs, int offset) throws InstantiationException, IllegalAccessException, InvocationTargetException, SQLException { Object[] args = new Object[c.getArgs().size()]; for (int i = 0; i < args.length; i++) { //其会结合Configuration去做对应的类型转换。 args[i] = get(rs, c.getArgs().get(i), offset + i + 1, c.getArgs().get(i).getType()); } //会调用FactoryExpression子类中对应的创建对象; return c.newInstance(args);}
复制代码
在我们常规下,我们用的 QBean 以及 QTuple 来做映射结果;我们分别来查看代码:
//QBean,通过反射来做属性的注入public T newInstance(Object... a) { try { T rv = create(getType()); if (fieldAccess) { for (int i = 0; i < a.length; i++) { Object value = a[i]; if (value != null) { Field field = fields.get(i); if (field != null) { field.set(rv, value); } } } } else { for (int i = 0; i < a.length; i++) { Object value = a[i]; if (value != null) { Method setter = setters.get(i); if (setter != null) { setter.invoke(rv, value); } } } } return rv; } catch (InstantiationException e) { //.... }}//QTuple@Overridepublic Tuple newInstance(Object... a) { return new TupleImpl(a);}
复制代码
3.2 基于 All
也就是 select * from 表名;
//AbstractSQLQuery.fetchif (expr.equals(Wildcard.all)) { while (rs.next()) { Object[] row = new Object[rs.getMetaData().getColumnCount()]; if (getLastCell) { lastCell = rs.getObject(row.length); getLastCell = false; } for (int i = 0; i < row.length; i++) { row[i] = rs.getObject(i + 1); } rv.add((T) row); }}
复制代码
3.3 基于单列
while (rs.next()) { if (getLastCell) { lastCell = rs.getObject(2); getLastCell = false; } rv.add(get(rs, expr, 1, expr.getType()));}
复制代码
四. 类型转换
在生产 SQL 中,对"?"占位符替换目标值以及结果映射中的属性值获取,都涉及到类型转换,然而并没有介绍,特此在这里进行完全介绍。
//Configurationpublic <T> void set(PreparedStatement stmt, Path<?> path, int i, T value) throws SQLException { if (value == null || value instanceof Null) { //如果是空对象,从表元数据中找到对应的列对象信息中的jdbcType类型, //调用setNull来设置 Integer sqlType = null; if (path != null) { ColumnMetadata columnMetadata = ColumnMetadata.getColumnMetadata(path); if (columnMetadata.hasJdbcType()) { sqlType = columnMetadata.getJdbcType(); } } if (sqlType != null) { stmt.setNull(i, sqlType); } else { stmt.setNull(i, Types.NULL); } } else { //如果不是空对象,则从javaType映射集合中找到对应的类型处理器. //其接口是com.querydsl.sql.types.Type,调用其setValue方法 getType(path, (Class) value.getClass()).setValue(stmt, i, value); }}//假设找到的类型LocalDate,那么对应的实现类JSR310LocalDateType//JSR310LocalDateType@Overridepublic void setValue(PreparedStatement st, int startIndex, LocalDate value) throws SQLException { Instant i = value.atStartOfDay(ZoneOffset.UTC).toInstant(); st.setDate(startIndex, new Date(i.toEpochMilli()), utc());}
复制代码
public <T> T get(ResultSet rs, @Nullable Path<?> path, int i, Class<T> clazz) throws SQLException { //其跟上面的介绍差不多,也是从javaType映射集合中找到对应的类型处理器,然后调用getValue方法 return getType(path, clazz).getValue(rs, i);}//假设找到的类型LocalDate,那么对应的实现类JSR310LocalDateType//JSR310LocalDateTypepublic LocalDate getValue(ResultSet rs, int startIndex) throws SQLException { Date date = rs.getDate(startIndex, utc()); return date != null ? LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneOffset.UTC).toLocalDate() : null;}
复制代码
五. Configuration
上面介绍了类型转换讲到了 Configuration,可以这么理解,这个配置对象影响了 QueryDSL 生成 SQL 以及结果映射的过程。该里面存放了哪些内容,该类有哪些属性对象,如图所示:
jdbcTypeMapping&javaTypeMapping 存放了 SQL 数据类型对应的 java 类型的映射以及类型转换器
nameMapping& schemaMapping 存放修正的表以及列的映射;
typeToName 类型名对应的 class 类型;目前没有找到其用途在哪。先忽略
template 目标数据库的模板语法。
exceptionTranslator 异常转换;
listeners 执行过程的监听器;
hasTableColumnTypes 是否有拓展表列对应的类型转换器
useLiterals 是否使用字面量去填充参数值;
六. 总结
这里介绍了 QueryDSL 的 sql 生成以及结果映射的关键逻辑,相信对其的 SQL 生成以及结果映射有了更进一步了解;同时也介绍了 Configuration 中的类型转换。
这里没有讲解 QueryFactory 接口以及其实现类,其是创建对应的 SQL 表达式的入口;
另外也没有对模板 Template 的介绍,直接去看代码即可大体看明白了;同时在生成 SQL 过程有关 QueryFlag 以及 JoinFlag 的是如何应该 SQL 生成的;
感兴趣的可以去查阅相关的代码;
评论