【问题标题】:Recursion in Fluent APIFluent API 中的递归
【发布时间】:2014-03-15 06:19:29
【问题描述】:

我正在设计一个流畅的 API 来编写 SQL。请记住,我的目标之一是让 API 不建议在链的该部分中无法调用的函数。例如,如果您刚刚在 select 子句中定义了一个字段,那么在您首先调用 From 之前,您不能调用 Where。一个简单的查询如下所示:

        string sql = SelectBuilder.Create()
            .Select()
                .Fld("field1")
            .From("table1")
            .Where()
                .Whr("field1 > field2")
                .Whr("CURRENT_TIMESTAMP > field3")
            .Build()
            .SQL;

我的问题来自 SQL 代码中的递归。假设你想让一个字段包含另一个 SQL 语句,如下所示:

        string sql = SelectBuilder.Create()
            .Select()
                .Fld("field1")
                .SQLFld()
                    .Select
                        .Count("field6")
                    .From("other table")
                .EndSQLFld()
                .FLd("field2")
            .From("table1")
            .Where()
                .Whr("field1 > field2")
                .Whr("CURRENT_TIMESTAMP > field3")
            .Build()
            .SQL;

我正在使用方法链接来构建我的流畅 API。它在很多方面都是一个状态机,分布在代表每个状态的许多类中。要添加此功能,我基本上需要复制我已经拥有的每个状态并将它们包装在两个 SQLFld 和 EndSQLFld 状态周围。如果您再下一层并将 SQL 语句嵌入到已经嵌入的 SQL 语句的字段中,我将需要另一个副本。这继续到无穷大,因此对于无限深的嵌入式 SQL 查询,我需要无限数量的类来表示无限状态。

我考虑编写一个 SelectBuilder 查询,该查询被带到 Build 方法的位置,然后将该 SelectBuilder 嵌入到另一个 SelectBuilder 中,从而解决了我的无穷大问题,但它不是很优雅,这就是这个 API 的重点.

我也可以抛弃 API 仅在适当时提供函数的想法,但我真的很讨厌这样做。我觉得这可以帮助您最好地发现如何使用 API。在许多流畅的 API 中,调用什么顺序并不重要,但我希望 API 尽可能接近实际的 SQL 语句并强制执行其语法。

有人知道如何解决这个问题吗?

【问题讨论】:

  • 我还以为你在谈论递归 CTE。请注意,子查询可以包含对外部环境的引用(实际上是 正常 情况)。子查询也可以用作表引用(FROM/JOIN 子句)和WHERE 子句。我希望您不只是将字符串连接在一起,否则人们可能会认为这是防 SQL 注入的(因此,您至少需要提供一个变量参数化方法)。当然,C# 已经有了 Linq-to-SQL,虽然有点不同。

标签: c# sql fluent


【解决方案1】:

很高兴看到您在尝试流畅的界面,我认为它们非常优雅且富有表现力。

构建器模式并不是流畅接口的唯一实现。考虑一下这个设计,让我们知道你的想法 =)

这是一个示例,我将最终实现的细节留给您。

界面设计示例:

 public class QueryDefinition
    {
        // The members doesn't need to be strings, can be whatever you use to handle the construction of the query.
        private string select;
        private string from;
        private string where;

        public QueryDefinition AddField(string select)
        {
            this.select = select;
            return this;
        }

        public QueryDefinition From(string from)
        {
            this.from = from;
            return this;
        }

        public QueryDefinition Where(string where)
        {
            this.where = where;
            return this;
        }

        public QueryDefinition AddFieldWithSubQuery(Action<QueryDefinition> definitionAction)
        {
            var subQueryDefinition = new QueryDefinition(); 
            definitionAction(subQueryDefinition);

            // Add here any action needed to consider the sub query, which should be defined in the object subQueryDefinition.

            return this;
        }

示例用法:

static void Main(string[] args)
        {
            // 1 query deep
            var def = new QueryDefinition();
            def
                .AddField("Field1")
                .AddField("Filed2")
                .AddFieldWithSubQuery(subquery =>
                {
                    subquery
                        .AddField("InnerField1")
                        .AddField("InnerFiled2")
                        .From("InnerTable")
                        .Where("<InnerCondition>");
                })
                .From("Table")
                .Where("<Condition>");


            // 2 queries deep
            var def2 = new QueryDefinition();
            def2
                .AddField("Field1")
                .AddField("Filed2")
                .AddFieldWithSubQuery(subquery =>
                {
                    subquery
                        .AddField("InnerField1")
                        .AddField("InnerField2")
                        .AddFieldWithSubQuery(subsubquery =>
                            {
                                subsubquery
                                    .AddField("InnerInnerField1")
                                    .AddField("InnerInnerField2")
                                    .From("InnerInnerTable")
                                    .Where("<InnerInnerCondition>");
                            })
                        .From("InnerInnerTable")
                        .Where("<InnerCondition>");
                })
                .From("Table")
                .Where("<Condition>");
        }

【讨论】:

  • 玩得很好。看起来还不错,完美解决了问题。我出于其他原因考虑过使用 lamdas,但它们也适合这个问题。
【解决方案2】:

如果没有子结构的子 API 或所有内部结构级别(SELECT 列、WHERE 子句中的表达式、子查询)的清晰括号/结尾,您就不能“只有可用的方法”。

即使这样,通过单个 API 运行它也需要它是有状态的和“模态的”,并带有“括号”方法,以跟踪你在 decl 中的下落。错误报告和正确处理这些将是乏味的。

对我来说,用“流利”的方法结束括号似乎不流利且丑陋。这将导致EndSelectEndWhereEndSubquery 等的丑陋外观。我更愿意将子结构(例如,用于选择的 SUBQUERY)构建到局部变量中并添加。

我不喜欢 EndSQLFld() 成语,它通过终止字段来隐式终止子查询。我更喜欢并猜测终止子查询本身会更好,因为子查询本身是嵌套结构的复杂部分,而不是字段。

说实话,尝试为“声明性”语言 (SQL) 强制执行“声明性”API 的排序似乎是在浪费时间。


可能我认为更接近理想用法:

SelectBuilder select = SelectBuilder.Create("CUSTOMER")
        .Column("ID")
        .Column("NAME")
        /*.From("CUSTOMER")*/      // look, I'm just going to promote this onto the constructor.
        .Where("field1 > field2")
        .Where("CURRENT_TIMESTAMP > field3");

SelectBuilder countSubquery = SelectBuilder.Create("ORDER")
        .Formula("count(*)");
        .Where("ORDER.FK_CUSTOMER = CUSTOMER.ID");
        .Where("STATUS = 'A'");

select.Formula( countSubquery, "ORDER_COUNT");
string sql = SelectBuilder.SQL;

向 Hibernate Criteria API 道歉 :)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-02-06
    • 1970-01-01
    • 2021-02-06
    • 2013-10-10
    • 1970-01-01
    • 2014-09-12
    • 1970-01-01
    • 2014-09-28
    相关资源
    最近更新 更多