Pohon sintaks dan alternatif LINQ saat berinteraksi dengan database SQL



Pada artikel ini, menggunakan contoh ekspresi logika sederhana, akan ditunjukkan apa itu pohon sintaks abstrak dan apa yang dapat Anda lakukan dengannya. Alternatif ekspresi LINQ untuk mengeksekusi kueri terhadap database SQL juga akan dipertimbangkan.



Kisah pengembang



Itu adalah proyek Legacy dan saya diminta untuk meningkatkan kemampuan pemfilteran "lanjutan" nya.



Mereka dulu punya sesuatu seperti ini:



Dan mereka menginginkan sesuatu seperti ini:





, «» :



CREATE PROCEDURE dbo.SomeContextUserFind
    (@ContextId int, @Filter nvarchar(max)) AS
BEGIN

DECLARE @sql nvarchar(max) = 
    N'SELECT U.UserId, U.FirstName, U.LastName
    FROM [User] U
    INNER JOIN [SomeContext] [C]
      ON ....
    WHERE [C].ContextId = @p1 AND ' + @Filter;

EXEC sp_executesql 

    @sql,
    N'@p1 int',
    @p1=@ContextId
END


, , :



string BuildFilter(IEnumerable<FilterItem> items)
{
    var builder = new StringBuilder();
    foreach (var item in items)
    {
        builder.Append(item.Field);
        bool isLike = false;
        switch (item.Operation)
        {
            case Operation.Equals:
                builder.Append(" = ");
                break;
            case Operation.Like:
                isLike = true;
                builder.Append(" LIKE ");
                break;
            //...
        }
        builder.Append('\'');
        if (isLike)
            builder.Append('%');
        builder.Append(Escape(item.Value));
        if (isLike)
            builder.Append('%');
        builder.Append('\'');
    }
    return builder.ToString();
}


, , - , , , ( ) . , , , - .



, , — «FilterItem» , , , .



« », , , , , .





-, , , :



[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Doe')


:





, , , :



abstract class Expr
{ }

class ExprColumn : Expr
{
    public string Name;
}

class ExprStr : Expr
{
    public string Value;
}

abstract class ExprBoolean : Expr
{ }

class ExprEqPredicate : ExprBoolean
{
    public ExprColumn Column;
    public ExprStr Value;
}

class ExprAnd : ExprBoolean
{
    public ExprBoolean Left;
    public ExprBoolean Right;
}

class ExprOr : ExprBoolean
{
    public ExprBoolean Left;
    public ExprBoolean Right;
}


, , :



var filterExpression = new ExprAnd
{
    Left = new ExprEqPredicate
    {
        Column = new ExprColumn
        {
            Name = "FirstName"
        },
        Value = new ExprStr
        {
            Value = "John"
        }
    },
    Right = new ExprOr
    {
        Left = new ExprEqPredicate
        {
            Column = new ExprColumn
            {
                Name = "LastName"
            },
            Value = new ExprStr
            {
                Value = "Smith"
            }
        },
        Right = new ExprEqPredicate
        {
            Column = new ExprColumn
            {
                Name = "LastName"
            },
            Value = new ExprStr
            {
                Value = "Doe"
            }
        }
    }
};


, , « », , «», .



« » — ( ) ( ) . . , ( ) :



<eqPredicate> ::= <column> = <str>
<or> ::= <eqPredicate>|or|<and> OR <eqPredicate>|or|<and>
<and> ::= <eqPredicate>|(<or>)|<and> AND <eqPredicate>|(<or>)|<and>


: «» , , , , . .



— , , , , .



SQL



, , .



— (Pattern Matching), :



var filterExpression = ...;

StringBuilder stringBuilder = new StringBuilder();
Match(filterExpression);

void Match(Expr expr)
{
    switch (expr)
    {
        case ExprColumn col:
            stringBuilder.Append('[' + Escape(col.Name, ']') + ']');
            break;
        case ExprStr str:
            stringBuilder.Append('\'' + Escape(str.Value, '\'') + '\'');
            break;
        case ExprEqPredicate predicate:
            Match(predicate.Column);
            stringBuilder.Append('=');
            Match(predicate.Value);
            break;
        case ExprOr or:
            Match(or.Left);
            stringBuilder.Append(" OR ");
            Match(or.Right);
            break;
        case ExprAnd and:
            ParenthesisForOr(and.Left);
            stringBuilder.Append(" AND ");
            ParenthesisForOr(and.Right);
            break;
    }
}

void ParenthesisForOr(ExprBoolean expr)
{
    if (expr is ExprOr)
    {
        stringBuilder.Append('(');
        Match(expr);
        stringBuilder.Append(')');
    }
    else
    {
        Match(expr);
    }
}


:



[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Joe')


, !



""



, - — « (Visitor)». , , ( ), , . :



interface IExprVisitor
{
    void VisitColumn(ExprColumn column);
    void VisitStr(ExprStr str);
    void VisitEqPredicate(ExprEqPredicate eqPredicate);
    void VisitOr(ExprOr or);
    void VisitAnd(ExprAnd and);
}


( ) :



abstract class Expr
{
    public abstract void Accept(IExprVisitor visitor);
}

class ExprColumn : Expr
{
    public string Name;

    public override void Accept(IExprVisitor visitor)
        => visitor.VisitColumn(this);
}

class ExprStr : Expr
{
    public string Value;

    public override void Accept(IExprVisitor visitor)
        => visitor.VisitStr(this);
}
...


sql :



class SqlBuilder : IExprVisitor
{
    private readonly StringBuilder _stringBuilder 
        = new StringBuilder();

    public string GetResult()
        => this._stringBuilder.ToString();

    public void VisitColumn(ExprColumn column)
    {
        this._stringBuilder
            .Append('[' + Escape(column.Name, ']') + ']');
    }

    public void VisitStr(ExprStr str)
    {
        this._stringBuilder
            .Append('\'' + Escape(str.Value, '\'') + '\'');
    }

    public void VisitEqPredicate(ExprEqPredicate eqPredicate)
    {
        eqPredicate.Column.Accept(this);
        this._stringBuilder.Append('=');
        eqPredicate.Value.Accept(this);
    }

    public void VisitAnd(ExprAnd and)
    {
        and.Left.Accept(this);
        this._stringBuilder.Append(" AND ");
        and.Right.Accept(this);
    }

    public void VisitOr(ExprOr or)
    {
        ParenthesisForOr(or.Left);
        this._stringBuilder.Append(" OR ");
        ParenthesisForOr(or.Right);
    }

    void ParenthesisForOr(ExprBoolean expr)
    {
        if (expr is ExprOr)
        {
            this._stringBuilder.Append('(');
            expr.Accept(this);
            this._stringBuilder.Append(')');
        }
        else
        {
            expr.Accept(this);
        }
    }

    private static string Escape(string str, char ch)
    {
        ...
    }
}


:



var filterExpression = BuildFilter();

var sqlBuilder = new SqlBuilder();
filterExpression.Accept(sqlBuilder);
string sql = sqlBuilder.GetResult();


:



[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Joe')


" (Visitor)" . , , , IExprVisitor , , ( ).





, .



-, ?



, , sql :





, , .



-, ?



, . «» , «» — . , . ( ), .





, , “NotEqual”, () , :



class ExprNotEqPredicate : ExprBoolean
{
    public ExprColumn Column;
    public ExprStr Value;

    public override void Accept(IExprVisitor visitor)
        => visitor.VisitNotEqPredicate(this);
}


, , SQL:



public void VisitNotEqPredicate(ExprNotEqPredicate exprNotEqPredicate)
{
    exprNotEqPredicate.Column.Accept(this);
    this._stringBuilder.Append("!=");
    exprNotEqPredicate.Value.Accept(this);
}


, , MS SQL , , , .



, SQL Server SQL, :





, . C#. :



class ExprColumn : Expr
{
    ...
    public static ExprBoolean operator==(ExprColumn column, string value)
        => new ExprEqPredicate 
        {
            Column = column, Value = new ExprStr {Value = value}
        };

    public static ExprBoolean operator !=(ExprColumn column, string value)
        => new ExprNotEqPredicate
        {
            Column = column, Value = new ExprStr {Value = value}
        };
}

abstract class ExprBoolean : Expr
{
    public static ExprBoolean operator &(ExprBoolean left, ExprBoolean right)
        => new ExprAnd{Left = left, Right = right};

    public static ExprBoolean operator |(ExprBoolean left, ExprBoolean right)
        => new ExprOr { Left = left, Right = right };
}


:



ExprColumn firstName = new ExprColumn{Name = "FirstName"};
ExprColumn lastName = new ExprColumn{Name = "LastName"};

var expr = firstName == "John" & (lastName == "Smith" | lastName == "Doe");

var builder = new SqlBuilder();
expr.Accept(builder);
var sql = builder.GetResult();


:



[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Doe')


: C# && ||, , , ( true || ....), ( SQL).





, , ? - - (View Derived Table) ( ) .



! SQL SELECT:



, , (, ), , SQL.



LINQ



, , LINQ, . C# , , Entity Framework «LINQ to SQL», SQL.



, C#, SQL! C# SQL – , .



— C# SQL. , .



, , , . , , intellisense - . … , LEFT JOIN LINQ)



, (INSERT, UPDATE, DELETE MERGE) , .



Hanya contoh dari apa yang bisa dilakukan menggunakan sintaks SQL nyata sebagai dasar ( diambil dari sini ):



await InsertInto(tCustomer, tCustomer.UserId)
    .From(
        Select(tUser.UserId)
            .From(tUser)
            .Where(!Exists(
                SelectOne()
                    .From(tSubCustomer)
                    .Where(tSubCustomer.UserId == tUser.UserId))))
    .Exec(database);


Kesimpulan



Pohon sintaks adalah struktur data yang sangat kuat yang mungkin akan Anda temui cepat atau lambat. Mungkin ekspresi LINQ, atau mungkin Anda perlu membuat pengurai Roslyn, atau mungkin Anda ingin membuat sintaks sendiri, seperti yang saya lakukan beberapa tahun lalu, untuk memfaktor ulang satu proyek lama. Bagaimanapun, penting untuk memahami struktur ini dan dapat bekerja dengannya.



Tautan ke SqExpress adalah proyek yang menyertakan sebagian kode dari artikel ini.




All Articles