自己動手實現Expression翻譯器 – Part Ⅱ


上一節我們了解了Linq查詢大體上是如何運轉的,並針對SQL表達式進行建模(DbExpression),這一節的重點在於如何將表達式轉換為DbExpression

可以說只要能生成結構清晰的DbExpression,我們的翻譯器就已經成功了一半了。為了將表達式轉換為DbExpression,我們需要遍歷它們,分解它們,在這個過程中拿出我們需要的信息,去構建一個個符合邏輯的DbExpression對象,最終將這些DbExpression組合起來,就形成了一個結構清晰的查詢。

一.表達式遍歷器(ExpressionVisitor)

相信不少人研究過ExpressionVisitor,它的作用是輸入一個Expression,將Expression拆解為各個組成部分(也是Expression),再針對這些組成部分繼續拆解,直到無法拆解,這是一個自頂向下的過程,每次拆解都是根據ExpressionType的不同而去調用專用的方法。

比如 x.Name == “灰機”這個BinaryExpression,將其輸入ExpressionVisitor的執行過程如下

Visitor過程

其中每個Visit方法都會將解析的結果返回,沒有重寫這些方法的話,ExpressionVisitor.Visit(Exp)的結果基本等於 Exp本身的,只是將它拆開又組合了一遍。

ExpressionVisitor幫我們做了很多工作,能准確的將特定類型的Expression傳遞給特定類型的方法去解析,並且它這些解析方法都是可重寫的,我們就繼承它,實現自己的解析邏輯。

二.數據庫表達式遍歷器(DbExpressionVisitor)

    /// <summary>
    /// 數據庫表達式遍歷器
    /// </summary>
    public class DbExpressionVisitor : ExpressionVisitor
    {
    }

首先重寫Visit方法提供對DbExpression的遍歷支持

public override Expression Visit(Expression exp)
{
    if (exp == null) return null;

    switch ((DbExpressionType)exp.NodeType)
    {
        case DbExpressionType.Select:
        case DbExpressionType.Table:
        case DbExpressionType.Join:
        case DbExpressionType.Query:
            return this.VisitQuery((QueryExpression)exp);
        case DbExpressionType.Column:
            return this.VisitColumn((ColumnExpression)exp);
    }

    return base.Visit(exp);
}

1.我們需要完成基礎的對IQueryable對象的解析,也就是說對一個IQueryable對象,沒有調用任何方法,我們的解析器應該解析出一個QueryExpression對象

先從最普通的TableExpression開始

//query代表的是對User表的整表查詢,應該解析為TableExpression
var query = new DbQuery<User>(provider);

先調試一下Execute方法看看query的Expression長啥樣

IQueryable.Expression

可以發現是一個ConstantExpression,那我們就去改寫VisitConstant方法

protected override Expression VisitConstant(ConstantExpression constant)
{
    var queryable = constant.Value as IQueryable;
    if (queryable != null)
    {
        //TableAttribute用來描述類對應的數據庫表信息
        var table = (TableAttribute)queryable.ElementType.GetCustomAttributes(typeof(TableAttribute), false).FirstOrDefault();
        //如果沒有該特性,直接使用類名作為表名
        var tableName = table == null ? queryable.ElementType.Name : table.Name;

        return new TableExpression(queryable.ElementType, string.Empty, tableName);
    }

    return base.VisitConstant(constant);
}

使用示例如下

 

TableExpression示例

這樣我們就得到了一個結構清晰的TableExpression對象,根據這個對象生成SQL語句是不是很容易?容易得很~Name、Columns都有,等等,Columns我們沒生成啊,我們只是得到了個表名啊喂,OK那我們就來做ColumnExpression的生成

快速回顧一下一個ColumnExpression的必要元素有哪些

image

SelectAlias -- 在SelectExpression中就是Select的別名,在TableExpression中應該是表名。

ColumnsName -- 列名

Index -- 排序

為什么需要Value呢,使用SelectAlias.ColumnName不就可以標識出列的全部信息了嗎?

這是因為有時候列不一定是從來源那里得到的,比如我可以

”query.Select( x => new {  Date = DateTime.Now,  x.UserName } )“

這個時候[Date]列是我們臨時構建的,我們需要保存DateTime.Now到Value里邊去,而[UserName]的Value應該是對[User]的一個列引用

既然TableExpression代表對一個表的全部列查詢,那我們就生成這個表的所有ColumnExpression好了,從哪里的得到這些信息呢?從映射類的定義

[Table(Name = "User")]
public class User
{
    [Column(IsPrimaryKey = true)]
    public int UserId { get; set; }

    [Column]
    public string UserName { get; set; }
}

這些信息從TableExpression.Type就可以得到了,上代碼(重載了QueryExpression.Columns)

/// <summary>
/// 表的列
/// </summary>
public override IEnumerable<ColumnExpression> Columns
{
    get
    {
        if (_columns == null)
        {
            int index = 0;
            _columns = new List<ColumnExpression>();
            var members = Type.GetProperties();
            var obj = Expression.Constant(Activator.CreateInstance(Type));
            foreach (var member in members)
            {
                _columns.Add(new ColumnExpression(member.PropertyType,
                    Expression.MakeMemberAccess(obj, member), Alias, member.Name, index++));
            }
        }

        return _columns;
    }
}

private List<ColumnExpression> _columns;

當然這里有很多可以優化的地方,但是現在先讓我們看看使用上述代碼后生成的結果

TableExpression.Columns示例

注意SelectAlias我賦了空,因為別名需要另外一些邏輯才能確定,不能單純使用表名。

這里每個ColumnExpression的Value都是一條MemberExpression,這樣我們就得到了詳細的元數據了。

 

三.數據庫表達式格式化器(QueryFormatter)

扯了這么多,終於可以寫點有用的了,我們需要對TableExpression再Visit一遍,這次的是要生成SQL語句了。

首先為了職責分明,讓我們再引入一個格式化器~

/// <summary>
/// 查詢語句格式化器
/// </summary>
public class QueryFormatter : DbExpressionVisitor
{
}

內部放置一個StringBuilder來拼接語句

private readonly StringBuilder _sb = new StringBuilder();

重載一下VisitTable

public override Expression VisitTable(TableExpression table)
{
    _sb.Append("(SELECT ");
    
  int index = 0;
    foreach (var column in table.Columns)
    {
        if (index++ > 0) _sb.Append(", ");
        this.VisitColumn(column);
    }
    _sb.AppendFormat(" FROM [{0}]", table.Name);
  if (!table.Alias.IsNullOrEmpty())
        _sb.AppendFormat(" As [{0}] ", table.Alias);

    _sb.Append(")");

    return table;
}

再重載一下VisitColumn

public override Expression VisitColumn(ColumnExpression column)
{
    var value = column.Value;
    switch (value.NodeType)
    {
        case ExpressionType.MemberAccess:
            if (!column.SelectAlias.IsNullOrEmpty())
                _sb.AppendFormat("[{0}].", column.SelectAlias);

            var member = ((MemberExpression)value).Member;
            if (member.Name == column.ColumnName)
                _sb.AppendFormat("[{0}]", column.ColumnName);
            else
                _sb.AppendFormat("[{0}] As [{1}]", member.Name, column.ColumnName);
            break;
        default:
            this.Visit(column.Value);
            _sb.AppendFormat(" As [{0}]", column.ColumnName);
            break;
    }

    return column;
}

只要將TableExpression傳遞給QueryFormatter.Visit(),QueryFormatter內部的_sb對象就擁有了完整的SQL語句,讓我們開放一個方法給外部調用驗證下結果先

public string Format(Expression expression)
{
    _sb.Clear();
    this.VisitQuery((QueryExpression)expression);
    return this._sb.ToString();
}

public override Expression VisitQuery(QueryExpression exp)
{
    switch (exp.DbExpressionType)
    {
        case DbExpressionType.Select: this.VisitSelect((SelectExpression)exp); break;
        case DbExpressionType.Table: this.VisitTable((TableExpression)exp); break;
        case DbExpressionType.Join: this.VisitSource((JoinExpression)exp); break;
    }

    return exp;
}

這里假設Expression已經被轉換為QueryExpression,畢竟沒有轉換的話我們這個格式化器是無法運轉的。

調用示例如下

TableExpression解析結果

團長我完成任務了!( •̀ ω •́ )y

四.下一步做什么

我還沒想好,等我對代碼稍作整理~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM