Drools 规则文件语法概述

概述(Overview)

以.drl为扩展名的文件,是Drools中的规则文件,规则文件的编写,遵循Drools规则语法。下面详细介绍一下Drools规则文件语法。具体参考官方文档: https://docs.jboss.org/drools/release/7.0.0.Final/drools-docs/html_single/index.html#_droolslanguagereferencechapter
DRL文件的整体结构如下:

package package-name

imports

globals

functions

queries

rules

对于上述元素,其顺序在drl文件中的顺序不重要,除了package-name,必须是drl文件的第一个元素。上述所有的元素都是可选的,接下来我们将详细介绍上述元素。

规则(Rule)组成

一个简单的规则结构如下所示:

rule "name"
    attributes
    when
        LHS
    then
        RHS
end

通常,规则文件中是不需要标点符号的。即使是规则名“name”上的双引号也是可选的。attributes展示规则的执行方式,也是可选的。LHS是规则的条件部分,遵循一个固定的语法规则,在下面的内容会详细介绍。RHS通常是一个可以本地执行的代码块。

关键字(Key Words)

从Drools 5开始,提出了“软”关键字和“硬”关键字的概念。硬关键字是保留的,不允许开发人员在编写规则文件时使用。硬关键字有如下几个:

  • true
  • false
  • null

软关键字是在文件内容中公认的一些字,开发人员可以在任何地方使用这些关键字,但是为了消除混淆,并不推荐开发人员在实际中使用这些关键字。软关键字有如下几个:

  • lock-on-active
  • date-effective
  • date-expires
  • no-loop
  • auto-focus
  • activation-group
  • agenda-group
  • ruleflow-group
  • entry-point
  • duration
  • package
  • import
  • dialect
  • salience
  • enabled
  • attributes
  • rule
  • extend
  • when
  • then
  • template
  • query
  • declare
  • function
  • global
  • eval
  • not
  • in
  • or
  • and
  • exists
  • forall
  • accumulate
  • collect
  • from
  • action
  • reverse
  • result
  • end
  • over
  • init

注释(Comments)

注释是规则文件中一段会被规则引擎自动忽略的一段文本。drl文件中的注释采用类Java语法的方式,可以分为两类:单行注释和多行注释。

单行注释

单行注释可以简单的使用双斜杠"//"来标识。语法解析器会自动忽视其后的所有内容。样例如下:

rule "Testing Comments"
when
    // this is a single line comment
    eval( true ) // this is a comment in the same line of a pattern
then
    // this is a comment inside a semantic code block
end

另外需要注意的是,“#”开头的单行注释在drl文件中已经被弃用。

多行注释

多行注释主要用于在代码块外对整个文件进行注释,以"/"开头和"/"结尾的之间所有的内容都会被语法解析器解释为注释。样例如下:

rule "Test Multi-line Comments"
when
    /* this is a multi-line comment
       in the left hand side of a rule */
    eval( true )
then
    /* and this is a multi-line comment
       in the right hand side of a rule */
end

错误信息(Error Messages)

Drools 5 开始提出了标准的错误信息。标准化的目的是为了使开发者可以更准确更简单定位错误问题所在。接下来将介绍如正确理解和识别错误信息。

错误信息格式

标准的错误信息格式如下所示。


ErrorMessageFormat.png

错误信息包含以下几个部分:

  • 1st Block: 表明当前错误的错误码。
  • 2st Block: 错误可能发生的行和列。
  • 3st Block: 错误信息描述。
  • 4st Block: 指明错误发生的规则,函数,模板,查询等。
  • 5st Block: 指明错误发生于何种模式。一般不是强制性的。

错误码描述

  • 101: No viable alternative
    错误码101指明了最常见的错误,语法解析器无法找到替代方案。下面有一些常见的例子:
1: rule one
2:   when
3:     exists Foo()
4:     exits Bar()  // "exits"
5:   then
6: end

上述示例会产生如下错误信息:

  • [ERR 101] Line 4:4 no viable alternative at input 'exits' in rule one

上述例子中的exits != exists, 解析器找不到exits的替代方案,于是报错。下面我们可以再看一个例子。

1: package org.drools.examples;
2: rule
3:   when
4:     Object()
5:   then
6:     System.out.println("A RHS");
7: end

现在,上述的代码会产生如下错误信息:

  • [ERR 101] Line 3:2 no viable alternative at input 'WHEN'

这里when是一个关键字,语法解析器在这里会遇到一个问题:rule没有文件名,而when是一个规则的条件部分。因此报错。下面还有一个相同类型错误的示例:

1: rule simple_rule
2:   when
3:     Student( name == "Andy )
4:   then
5: end

这里双引号缺少了另一半,因此会报错。

  • 102: Mismatched input
    该错误表明,语法解析器在当前输入位置中未找到一个特定的符号。下面有一些例子:
1: rule simple_rule
2:   when
3:     foo3 : Bar(

上述示例会产生如下错误:

  • [ERR 102] Line 0:-1 mismatched input '<eof>' expecting ')' in rule simple_rule in pattern Bar

要解决上述问题,需要完善上述规则语句。接下来的实例会产生多个错误:

1: package org.drools.examples;
2:
3: rule "Avoid NPE on wrong syntax"
4:   when
5:     not( Cheese( ( type == "stilton", price == 10 ) || ( type == "brie", price == 15 ) ) from $cheeseList )
6:   then
7:     System.out.println("OK");
8: end

上述代码会产生如下错误:

  • [ERR 102] Line 5:36 mismatched input ',' expecting ')' in rule "Avoid NPE on wrong syntax" in pattern Cheese
  • [ERR 101] Line 5:57 no viable alternative at input 'type' in rule "Avoid NPE on wrong syntax

 该错误信息与前一个错误相关,这里只需要将```,``````&&```替换就好了。

103: Failed predicate*
出现该错误信息的原因是一个验证的语义谓词被验证为false。通常这些语义谓词用于识别软关键字。以下是一个示例:

1: package nesting;
2: dialect "mvel"
3:
4: import org.drools.compiler.Person
5: import org.drools.compiler.Address
6:
7: fdsfdsfds
8:
9: rule "test something"
10: when
11: p: Person( name=="Michael" )
12: then
13: p.name = "other";
14: System.out.println(p.name);
15: end

我们可以得到如下错误信息:
  * ```[ERR 103] Line 7:0 rule 'rule_key' failed predicate: {(validateIdentifierKey(DroolsSoftKeywords.RULE))}? in rule

fdsfdsfds是个无效的关键字,语法解析器无法将其识别成一个软关键字。

  • 104: Trailing semi-colon not allowed
    该错误信息与eval从句相关,当分号不是出现在其表达式的结尾时可能报此错误。可以看一下如下示例。
1: rule simple_rule
2:   when
3:     eval(abc();)
4:   then
5: end

由于在eval中以分号结尾,我们可以得到如下错误信息:

  • [ERR 104] Line 3:4 trailing semi-colon not allowed in rule simple_rule

该错误很容易修复,只要移除eval中的分号即可。

  • 105: Early Exit
    drl文件中的子规则至少能被匹配选择一次,但是当子规则不能匹配任何东西的时候,就会报这个错。
    以下是一个示例:
1: template test_error
2:   aa s  11;
3: end

该示例会报以下错误信息:

  • [ERR 105] Line 2:2 required (…​)+ loop did not match anything at input 'aa' in template test_error

  • Other Messages
    开发中可能还会遇到一些其他意想不到的问题,这些问题可以向Drools的开发团队求助。

包(Package)

package是一系列rule或其他相关构件如imports, globals的容器。这个成员之间相互关联,一个package代表了一个命名空间,其中的每个rule的名字都是唯一的,package名字本身就是命名空间,与实际的文件和文件夹没有任何关系。常见的结构是,一个文件包含多个rule的文件就定义成一个包。以下的线路图表明了一个包中包含的所有组成元素。

package.png

   需要注意的是,一个包必须有一个命名空间,且其声明必须遵守Java命名规范。package语句必须出现在包的首行,其他组成部分的出现顺序无关紧要。其中package语句结尾的分号;是可选的。

import

Drools文件中的import语句功能与Java中的import语句功能类似。使用import时,必须指定对象的全称限定路径和类型名。Drools会自动导入Java相同名字的包中的所有类,也会导入java.lang.*

global

global.png

   global用于定义全局变量。用于使应用对象对一系列规则有效。通常,用于向规则提供全局的数据和服务,特别是一些用于规则序列的应用服务,如日志、规则序列中累加的值等。全局的变量是不会插入Woking Memory中的,另外,全局变量不要用于建立规则的条件部分,除非它是一个不会改变的常量。全部变量的改变不会通知到规则引擎,规则引擎不跟踪全局变量的变化,因为他们并没有加入到Woking Memory中。全局变量使用不当,会产生很多不可思议的结果。如果多个包中同时定义了相同标识符的全局变量,那么这些全局变量必须是相同类型,并会引用一个相同的全局值。为了更好地使用全局变量,必须遵循以下规则:

  1. 在规则文件中定义常量,并使用这些常量。
global java.util.List myGlobalList;
rule "Using a global"
when
    eval( true )
then
    myGlobalList.add( "Hello World" );
end
  1. 在工作内存中,为全局变量设值。在从内存中获取所有的fact之前,最好将所有的全局变量设置。例如:
List list = new ArrayList();
KieSession kieSession = kiebase.newKieSession();
kieSession.setGlobal( "myGlobalList", list );

全局变量并不是用来在规则间共享数据,而且最好不要用于在规则间共享数据。规则总是对工作内存的状态产生推理和反应,因此,如果想在规则之间传递数据,可以将这些数据作为facts传入工作内存。因为规则引擎并不会关心和跟踪这些全局变量的变化。

函数(Function)

function.png

   function提供了一种在规则源文件中插入语义代码的方式,与在普通Java类中不同。他们需要帮助类,否则不能做任何事情。(实际上,编译器会针对这些句子自动产生帮助类。)在规则中使用函数的最主要优点就是你可以把所有逻辑放在一个地方,你可以根据需要更改这些函数的逻辑。函数通常用于在规则的then部分调用某些动作,特别是一些经常被用到的而传入参数不一样的动作。
典型的function格式如下:

function String hello(String name) {
    return "Hello "+name+"!";
}

需要注意的是,这里我们使用了function关键字,即使它并不是Java语言的一部分。参数和返回值的定义和传入与Java语言一致。当然,参数和返回值并不是必须的。另外,我们可以使用一个帮助类中的静态方法,如:Foo.hello()
  Drools支持函数的导入,我们所要做的就是:

import function my.package.Foo.hello

不需要考虑函数的定义和导入方式,我们可以直接在需要的地方直接使用函数名调用这个函数。例如:

rule "using a static function"
when
    eval( true )
then
    System.out.println( hello( "Bob" ) );
end

类型声明(Type Declaration)

在规则引擎中,类型声明有两个目的:允许新的类型声明;允许元数据类型的声明。

  • 声明新类型:Drools工作时会将外部的普通Java对象作为事实。但是有时候使用者想要自己定义一些规则引擎可以直接使用的模型,而不用担心用Java之类的底层语言来创建对象。另外有时候,当一个域模型已经建立,但是用户想或者需要用一些主要在推理过程中使用的实体来完善这个模型。
  • 声明元数据:事实往往会有一些与之相关的元数据信息。元信息的样本包含的任何种类的数据都不能代表事实的属性,且在该事实类型的所有实例中都是不变的。这些元信息在规则引擎的运行和推理古城中需要被查询。

声明新类型

为了定义新类型,我们需要使用关键字``declare,然后是一系列域,最终以end关键字结尾。一个新的fact必须有一系列域,否则规则引擎会去classpath中寻找一个存在的fact```类,如果没找到,会报错。下面给出定义新类型的几个例子。

Example . Declaring a new fact type: Address

declare Address
   number : int
   streetName : String
   city : String
end

上面的例子中我们定义了一个新类型Address,这个fact有三个属性,每个属性都具有一个Java中有效的数据类型。同样的,我们可以再定义一个数据类型:
Example . Declaring a new fact type: Person

declare Person
    name : String
    dateOfBirth : java.util.Date
    address : Address
end

我们可以看一下上面的例子,dateOfBirth是Java中的java.util.Date类型,address是我们刚才声明的类型。为了不用写全称限定名,我们可以先使用import来导入要使用的类型,例如:
Avoiding the need to use fully qualified class names by using import

import java.util.Date

declare Person
name : String
dateOfBirth : Date
address : Address
end

当我们声明一个新的fact类型时,Drools会在编译期间生成实现自一个表示该fact类型的Java类的字节码。这个生成的Java类

Example : generated Java class for the previous Person fact typedeclaration

public class Person implements Serializable {
    private String name;
    private java.util.Date dateOfBirth;
    private Address address;
<span class="token comment">// empty constructor</span>
<span class="token keyword">public</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">}</span>

<span class="token comment">// constructor with all fields</span>
<span class="token keyword">public</span> <span class="token class-name">Person</span><span class="token punctuation">(</span> <span class="token class-name">String</span> name<span class="token punctuation">,</span> <span class="token class-name">Date</span> dateOfBirth<span class="token punctuation">,</span> <span class="token class-name">Address</span> address <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">}</span>

<span class="token comment">// if keys are defined, constructor with keys</span>
<span class="token keyword">public</span> <span class="token class-name">Person</span><span class="token punctuation">(</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>keys<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">}</span>

<span class="token comment">// getters and setters</span>
<span class="token comment">// equals/hashCode</span>
<span class="token comment">// toString</span>

}

该类型生成的class是一个普通的Java类,可以在规则中直接使用,就向其他 fact一样。见如下例子:
Using the declared types in rules

rule "Using a declared Type"
when
    $p : Person( name == "Bob" )
then
    // Insert Mark, who is Bob's mate.
    Person mark = new Person();
    mark.setName("Mark");
    insert( mark );
end

声明枚举类型

DRL同时支持声明枚举类型。该类型声明需要另外一种关键字enum,然后以都好分割可接收值的列表,最后以分号结束。

declare enum DaysOfWeek
   SUN("Sunday"),MON("Monday"),TUE("Tuesday"),WED("Wednesday"),THU("Thursday"),FRI("Friday"),SAT("Saturday");

fullName : String
end

声明完成之后,该枚举类型可以用于之后的规则中。

rule "Using a declared Enum"
when
   $p : Employee( dayOff == DaysOfWeek.MONDAY )
then
   ...
end

声明元数据

在Drools中元数据会被分配给一系列不同对象的构造:fact类型,fact属性和规则。Drools使用@符号来引出元数据,使用使用如下格式:

@metadata_key( metadata_value )

其中metadata_value是可选的。
   Drools允许声明任何任意元数据属性,但是当其他属性在运行时仅仅对查询有效时,有些属性对于规则引擎来说具有不同的意义。Drools允许为fact类型和fact属性声明元数据。所有的元数据在该属性被分配到fact类型前声明,而在向一个特定属性分配值之前声明。
Example 115. Declaring metadata attributes for fact types and attributes

import java.util.Date

declare Person
@author( Bob )
@dateOfCreation( 01-Feb-2009 )

name <span class="token operator">:</span> String <span class="token label symbol">@key</span> <span class="token label symbol">@maxLength</span><span class="token punctuation">(</span> <span class="token number">30</span> <span class="token punctuation">)</span>
dateOfBirth <span class="token operator">:</span> Date
address <span class="token operator">:</span> Address

end

上面的例子中,声明了两个fact类型(@author@dateOfCreation)的元数据元素,另外为name属性声明了两个(@key@maxLength)元数据。其中@key没有必须值,所有括号和值均被省略了。

规则(Rule)

rule.png

   一个规则,指定当(when)一系列特定的条件发生时(左手边LHS),然后(then)做出一系列相应的动作(右手边RHS)。一个常见的问题就是:为什么使用when而不是if,因为if通常是执行流程中特定时间点的一部分,是一个需要检查的条件。相反的,when指明的是一个不绑定于任何特定判断序列或时间点的条件判断,它在规则引擎的声明周期内的任何一个条件发生的情况下都可能触发,不管这个条件是否遇到,这些动作都会执行。
   一个规则在一个包中必须具有独一无二的名字。如果在一个DRL文件中重复定义两个相同名字的规则,在加载的时候就会报错。如果向包中添加一个名字已经存在的规则,该规则会覆盖掉之前的同名规则。如果一个规则命中存在空格符,最好使用双引号将规则名包括起来。
   规则的属性不是必须的,且属性最好写成一行。
   规则中的LHS在关键字when的后面,同样的,RHS应该在关键字then的后面,规则最后以关键字end结尾。另外,规则不准嵌套。

Example . Rule Syntax Overview

rule "<name>"
    <attribute>*
when
    <conditional element>*
then
    <action>*
end

Example . A simple rule

rule "Approve if not rejected"
  salience -100
  agenda-group "approval"
    when
        not Rejection()
        p : Policy(approved == false, policyState:status)
        exists Driver(age > 25)
        Process(status == policyState)
    then
        log("APPROVED: due to no objections.");
        p.setApproved(true);
end
rule attributes.png

规则属性

规则属性显式地声明了对规则行为的影响,有些规则属性很简单,有些规则属性是复杂的子系统的一部分,如规则流。为了从Drools中获得更多东西,我们需要确保对每一个规则属性均有正确的认识。
常用的规则属性有如下:

  • no-loop

    • 默认值:false
    • type: Boolean
      当规则序列更改了一个fact,会导致该规则会被重新触发,以至于产生一个无限循环。当设置为true时,当前规则只会被激活一次。
  • ruleflow-group

    • 默认值:N/A
    • type: String
      ruleflow是Drools的特色之一,可以让你自己控制规则的命中。同一个ruleflow-group中的所有规则只有当该组激活时才能被命中。
  • lock-on-active

    • 默认值:false
    • type: Boolean
      不管何时ruleflow-groupagenda-group被激活,只要其中的所有规则将lock-on-active设置为true,那么这些规则都不会再被激活,不管一开始怎么更新,这些匹配的规则都不会被激活。这是no-loop属性的增强,因为这些变化现在不仅仅是规则自身的变化。
  • salience

    • 默认值:0
    • type: Integer
      任何规则都有一个默认为0的salience属性,该属性可以为0,正数和负数。salience表示规则的优先级,值越大其在激活队列中的优先级越高。Drools支持使用动态的salience,可以使用一个包含动态约束变量的表达式来表示。如下所示
      Dynamic Salience
rule "Fire in rank order 1,2,.."
        salience( -$rank )
    when
        Element( $rank : rank,... )
    then
        ...
end
  • agenda-group

    • 默认值:MAIN
    • type: String
      agenda-group允许用户将Agenda分割成多个部分以提供更多的运行控制。
  • auto-focus

    • 默认值:false
    • type: Boolean
      当一个规则被激活时auto-focus为true,而且该规则的agenda-group还没有focus,当该agenda-groupfocus时,允许该规则潜在命中。
  • activation-group

    • 默认值:N/A
    • type: String
      属于同一个activation-group的规则会进行唯一命中。也就是说同一个activation-group中的规则,只要有一个命中,其他的规则都会被取消激活状态,这样这些规则就不会被命中。
  • dialect

    • 默认值:as specified by the package
    • type: String
      dialect用于指明规则中使用的代码的语言种类,目前支持两种语言,"java"或"mvel"。
  • date-effective

    • 默认值:N/A
    • type: String (包含日期和时间)
      当前系统时间在date-effective之后,该规则才会被激活。
  • date-effective

    • 默认值:N/A
    • type: String (包含日期和时间)
      当前系统时间在date-effective之后,该规则不会再被激活。
  • duration

    • 默认值:无
    • type: long (包含日期和时间)
      duration用于表示一个规则在一定时间之后才会被命中,如果它还是激活状态的话。

LHS语法

LHS是规则的条件部分的统称,由零到多条条件元素组成。如果LHS为空,默认为是条件部分一直为true。当一个新的WorkingMemory session创建的时候,会被激活和触发。
Example. Rule without a Conditional Element

rule "no CEs"
when
    // empty
then
    ... // actions (executed once)
end

// The above rule is internally rewritten as:

rule "eval(true)"
when
eval( true )
then
... // actions (executed once)
end

LHS中的条件元素基于一个或多个模式,最常用的条件元素是and。当然如果LHS中有多个不互相连接的模式时,默认使用隐式的and
Implicit and

rule "2 unconnected patterns"
when
    Pattern1()
    Pattern2()
then
    ... // actions
end

// The above rule is internally rewritten as:

rule "2 and connected patterns"
when
Pattern1()
and Pattern2()
then
... // actions
end

模式
模式是最终要的条件元素,它可以隐式地匹配所有插入到WorkingMemory中的所有fact。一个模式具有0个或多个约束条件和一个可选的模式组合。模式的结构图如下所示:

Pattern.png

下面给出一个最简单的模式的例子

Person()

这里的类型为Person,该模式意味着将匹配WorkingMemory中的所有Person对象。该类型不需要是一个真实 fact对象的类。模式可以指向超类甚至是接口,这样可以匹配多个不同类的facts。例如:

Object() // matches all objects in the working memory

模式的括号中条件定义了模式在何种条件下满足。如下所示:

Person( age == 100 )

为了引用匹配的对象,可以使用一个模式绑定参数如:$p
Example . Pattern with a binding variable

rule ...
when
    $p : Person()
then
    System.out.println( "Person " + $p );
end

$符号是非强制性的,只是用于在复杂的规则中方便标识,将其与变量及域区分开来。
约束

  • 什么是约束?

约束是一个返回truefalse的表达式,如下例所示:

Person( 5 < 6 )  // just an example, as constraints like this would be useless in a real pattern

约束本质上是一个与Java表达式稍微有点不同的表达式,例如equals()等价于==。接下来我们深入理解一下。

  • Java Beans属性获取。

任何一个bean的属性都可以被直接使用,bean属性的获取也可以使用标准的Java bean getter: getMyProperty() or isMyProperty()。例如:

//use directly
Person( age == 50 )

// this is the same as:
Person( getAge() == 50 )

同时,Drools还支持嵌套的属性获取方式,如:

//use directly
Person( address.houseNumber == 50 )

// this is the same as:
Person( getAddress().getHouseNumber() == 50 )

当然,约束中的条件表达式是支持Java表达式的,下面几个例子都是正确的:

Person( age == 50 )
Person( age > 100 && ( age % 10 == 0 ) )
Person( Math.round( weight / ( height * height ) ) < 25.0 )
  • 逗号分隔符 AND

逗号分隔约束,具有隐含的AND的含义。

// Person is at least 50 and weighs at least 80 kg
Person( age > 50, weight > 80 )

// Person is at least 50, weighs at least 80 kg and is taller than 2 meter.
Person( age > 50, weight > 80, height > 2 )

逗号运算符不能出现在复合的约束表达式中,如

// Do NOT do this: compile error
Person( ( age > 50, weight > 80 ) || height > 2 ) 

// Use this instead
Person( ( age > 50 && weight > 80 ) || height > 2 )

  • 绑定变量

属性值可以绑定到一个变量中:

// 2 persons of the same age
Person( $firstAge : age ) // binding
Person( age == $firstAge ) // constraint expression
  • 分组访问嵌套对象属性

可以先看一个例子:

Person( name == "mark", address.city == "london", address.country == "uk" )

Person( name == "mark", address.( city == "london", country == "uk") )

也就是对嵌套对象属性的访问,可以组合在一个括号里面。

  • 内联强制类型转换

当处理嵌套对象时,往往需要将其转换成子类,可以通过使用#符号来完成。如下例所示:

Person( name == "mark", address#LongAddress.country == "uk" )

在该例子中将 Address转换成LongAddress。如果类型转换失败,该值会被认为是false。当然,类型转换也支持全称限定名称。如下所示:

Person( name == "mark", address#org.domain.LongAddress.country == "uk" )

当然,在同一个表达式中使用多级内联转换也是可行的。如下所示:

Person( name == "mark", address#LongAddress.country#DetailedCountry.population > 10000000 )

另外,Drools同样支持instanceof操作。

Person( name == "mark", address instanceof LongAddress, address.country == "uk" )
  • 特殊文字支持
    除了正常的Java文字,Drools还支持以下特殊的文字:
    • 日期文字。

查询(Query)

Domain Specific Languages

原文地址:https://www.jianshu.com/p/ae9a62588da4
posted @ 2019-12-10 14:44  星朝  阅读(1867)  评论(0编辑  收藏  举报