AScript定制left/right join查询语法

AScript是一个开源的C#动态脚本解析执行引擎,支持扩展自定义语法。

在之前的文章《AScript如何实现LINQ语法》中实现了C#标准LINQ查询语法,但并不支持left join/right join语句,不过.NET10在Queryable/Enumerable中新增了LeftJoin/RightJoin扩展方法(LINQ查询语法中仍然不支持left join/right join写法)。

本篇将在LINQ语法的基础上增加left joinright join语法,以提高脚本灵活性。

一、left join

标准LINQ查询的左连接写法如下:

复制代码
1 from p in context.Persons
2 join a in context.AddressInfos on p.Id equals a.UserId into aa
3 from a in aa.DefaultIfEmpty()
4 select new { p.Id, p.Name, p.Age, MyAddress = a.Address };

简化后的left join语法如下:

复制代码
1 from p in context.Persons
2 left join a in context.AddressInfos on p.Id equals a.UserId
3 select new { p.Id, p.Name, p.Age, MyAddress = a.Address };

如何实现呢?

1、在QueryNode中添加AddLeftJoin方法

复制代码
1 public void AddLeftJoin(string varName, ITreeNode source, ITreeNode key1, ITreeNode key2)
2 {
3     string name1 = $"___{varName}___";
4     AddJoin(varName, source, key1, key2, name1);
5     AddFrom(varName, new CallFuncNode { Name = "DefaultIfEmpty", Args = new ITreeNode[] { new VariableNode(name1) } });
6 }

可以看到,AddLeftJoin方法中我们手动增加了join ... into xxxfrom ... xxx.DefaultIfEmpty()语句。

2、在FromTokenHandler中添加left join解析

复制代码
 1 public void Build(DefaultSyntaxAnalyzer analyzer, TokenAnalyzingArgs e)
 2 {
 3     e.IsHandled = true;
 4     var queryNode = e.Ignore ? null : new QueryNode();
 5     var createFullOptions = (e.Options.CreateFullTreeNode ?? false) ? e.Options : new BuildOptions(e.Options) { CreateFullTreeNode = true };
 6     // 解析from语句
 7     BuildFrom(analyzer, e, createFullOptions, queryNode);
 8     // 解析后续linq语句
 9     while (true)
10     {
11         var token = e.TokenReader.Read();
12         ...
13         else if (token.Value.IsSymbol("join"))
14         {
15             BuildJoin(analyzer, e, createFullOptions, queryNode);
16         }
17         else if (token.Value.IsSymbol("left"))
18         {
19             BuildLeftJoin(analyzer, e, createFullOptions, queryNode);
20         }
21         else if (token.Value.IsSymbol("right"))
22         {
23             BuildRightJoin(analyzer, e, createFullOptions, queryNode);
24         }
25         ...
26     }
27     // 将LINQ语句添加到语法树中
28     if (!e.Ignore)
29     {
30         e.TreeBuilder.AddData(e.BuildContext, e.ScriptContext, e.Options, e.Control, queryNode);
31     }
32 }
33 
34 private void BuildLeftJoin(DefaultSyntaxAnalyzer analyzer, TokenAnalyzingArgs e, BuildOptions createFullOptions, QueryNode queryNode)
35 {
36     analyzer.ValidateNextToken(e.TokenReader, "join");
37 
38     var varToken = analyzer.ValidateNextToken(e.TokenReader, ETokenType.Word);
39     analyzer.ValidateNextToken(e.TokenReader, "in");
40 
41     var source = analyzer.BuildOneStatement(e.BuildContext, e.ScriptContext, e.Options, e.TokenReader, e.Control, e.Ignore, _OnTokens);
42 
43     analyzer.ValidateNextToken(e.TokenReader, "on");
44 
45     var key1 = analyzer.BuildOneStatement(e.BuildContext, e.ScriptContext, createFullOptions, e.TokenReader, e.Control, e.Ignore, _EqualsTokens);
46     analyzer.ValidateNextToken(e.TokenReader, "equals");
47     var key2 = analyzer.BuildOneStatement(e.BuildContext, e.ScriptContext, createFullOptions, e.TokenReader, e.Control, e.Ignore, _Join2EndTokens);
48 
49     queryNode?.AddLeftJoin(varToken.Value.Value, source, key1, key2);
50 }

这样我们就实现了left join语法,以后在脚本中写左连接语句就方便多了。
sqlite查询完整示例

复制代码
 1 using (var context = new TestSqliteContext())
 2 {
 3     string s = @"
 4 var q = from p in context.Persons
 5     left join a in context.AddressInfos on p.Id equals a.UserId
 6     select new { p.Id, p.Name, p.Age, MyAddress = a.Address };
 7 q.ToList();
 8 ";
 9     var script = new Script();
10     script.Context.SetVar("context", context);
11     var list = script.Eval(s);
12     Console.WriteLine(JsonConvert.SerializeObject(list, Formatting.Indented));
13 }

生成的sqlite查询语句为:

复制代码
1 SELECT "p"."Id", "p"."Name", "p"."Age", "a"."Address" AS "MyAddress"
2    FROM "Persons" AS "p"
3    LEFT JOIN "AddressInfos" AS "a" ON "p"."Id" = "a"."UserId"

二、right join

标准LINQ查询要实现右连接,只能用左连接语句并交换2个表的位置来实现右连接功能。

比如上面的示例中把左连接AddressInfos表改为右连接,则交换2个表位置:

复制代码
1 from a in context.AddressInfos
2 left join p in context.Persons on a.UserId equals p.Id
3 select new { p.Id, p.Name, p?.Age, MyAddress = a.Address };

虽然能实现右连接效果,但是写法不太符合我们的逻辑,使用right join语法如下:

复制代码
1 from p in context.Persons
2 right join a in context.AddressInfos on p.Id equals a.UserId
3 select new { p.Id, p.Name, p?.Age, MyAddress = a.Address };

如何实现right join语法呢?

1、在QueryNode中添加AddRightJoin方法

复制代码
1 public void AddRightJoin(string varName, ITreeNode source, ITreeNode key1, ITreeNode key2)
2 {
3     var right = _Source;
4     var rightName = _CurrentVarName;
5     _Source = source;
6     _CurrentVarName = varName;
7     AddLeftJoin(rightName, right, key2, key1);
8 }

没错,就是交换2个表的位置使用左连接来实现。

2、在FromTokenHandler中添加right join解析

复制代码
 1 private void BuildRightJoin(DefaultSyntaxAnalyzer analyzer, TokenAnalyzingArgs e, BuildOptions createFullOptions, QueryNode queryNode)
 2 {
 3     analyzer.ValidateNextToken(e.TokenReader, "join");
 4 
 5     var varToken = analyzer.ValidateNextToken(e.TokenReader, ETokenType.Word);
 6     analyzer.ValidateNextToken(e.TokenReader, "in");
 7 
 8     var source = analyzer.BuildOneStatement(e.BuildContext, e.ScriptContext, e.Options, e.TokenReader, e.Control, e.Ignore, _OnTokens);
 9 
10     analyzer.ValidateNextToken(e.TokenReader, "on");
11 
12     var key1 = analyzer.BuildOneStatement(e.BuildContext, e.ScriptContext, createFullOptions, e.TokenReader, e.Control, e.Ignore, _EqualsTokens);
13     analyzer.ValidateNextToken(e.TokenReader, "equals");
14     var key2 = analyzer.BuildOneStatement(e.BuildContext, e.ScriptContext, createFullOptions, e.TokenReader, e.Control, e.Ignore, _Join2EndTokens);
15 
16     queryNode?.AddRightJoin(varToken.Value.Value, source, key1, key2);
17 }

sqlite查询完整示例

复制代码
 1 using (var context = new TestSqliteContext())
 2 {
 3     string s = @"
 4 var q = from p in context.Persons
 5     right join a in context.AddressInfos on p.Id equals a.UserId
 6     select new { p.Id, p.Name, p?.Age, MyAddress = a.Address };
 7 q.ToList();
 8 ";
 9     var script = new Script();
10     script.Context.SetVar("context", context);
11     var list = script.Eval(s);
12     Console.WriteLine(JsonConvert.SerializeObject(list, Formatting.Indented));
13 }

生成的sqlit查询语句为(左连接交换2个表位置):

复制代码
1 SELECT "p"."Id", "p"."Name", "p"."Age", "a"."Address" AS "MyAddress"
2     FROM "AddressInfos" AS "a"
3     LEFT JOIN "Persons" AS "p" ON "a"."UserId" = "p"."Id"

三、结束语

虽然是2个小小的语法,却大大提高了我们的脚本编写效率。

AScript开源地址:https://gitee.com/rockey627/AScript