如何校驗記憶體資料的一致性,DynamicExpresso 算是幫上大忙了

一線碼農發表於2020-08-19

一:背景

1. 講故事

記的在上一家公司做全記憶體專案的時候,因為一些關鍵表會在程式 startup 的時候全量灌入到記憶體中,但隨著時間的推移,記憶體和資料庫的同步偶爾會出現資料差異的情況,伴隨著就是運營那邊報過來的 bug,檢查資料庫的資料完整性很簡單,直接寫一些 sql 驗證一下就好了,但校驗記憶體中的資料就非常麻煩了,因為你不能像寫 sql 一樣直接去查生產中的記憶體集合,那怎麼辦呢? 為了方便演示問題,先上一段演示程式碼:


    class Program
    {
        static void Main(string[] args)
        {
            var tradeList = new List<Trade>()
            {
                new Trade(){TradeID=1, TradeTitle="交易1", Created=Convert.ToDateTime("2020/8/1"), CustomerID=1},
                new Trade(){TradeID=2, TradeTitle="交易2", Created=Convert.ToDateTime("2020/8/5"),CustomerID=2},
                new Trade(){TradeID=3, TradeTitle="交易3", Created=Convert.ToDateTime("2020/8/10"), CustomerID=3}
            };
        }
    }

    class Trade
    {
        public int TradeID { get; set; }

        public string TradeTitle { get; set; }

        public DateTime Created { get; set; }

        public int CustomerID { get; set; }
    }

上面的 tradeList 就是記憶體中的集合,現在有一個問題,我想查詢一下 trade 表中 CustomerID in (1,2,10) && Created <= '2020-08-01' 的記錄是否和記憶體中的 tradelist 一致。

用 sql 驗證太簡單了,直接在查詢分析器裡面寫一下sql 搞定,如下圖:

那在 UI 上 怎麼驗證呢?

二: 尋找解決方法

1. 在UI上自定義高階查詢

這個也是大家最容易想到的,使用多個 if 疊加查詢條件,如下程式碼所示:


        static void Main(string[] args)
        {
            var tradeList = new List<Trade>()
            {
                new Trade(){TradeID=1, TradeTitle="交易1", Created=Convert.ToDateTime("2020/8/1"), CustomerID=1},
                new Trade(){TradeID=2, TradeTitle="交易2", Created=Convert.ToDateTime("2020/8/5"),CustomerID=2},
                new Trade(){TradeID=3, TradeTitle="交易3", Created=Convert.ToDateTime("2020/8/10"), CustomerID=3}
            };

            IEnumerable<Trade> query = tradeList;

            //UI
            var queryCustomerIDList = new List<int>() { 1, 2, 10};
            var queryCreated = "2020-08-01";

            if (queryCustomerIDList.Count > 0)
            {
                query = query.Where(m => queryCustomerIDList.Contains(m.CustomerID));
            }
            if (string.IsNullOrEmpty(queryCreated))
            {
                query = query.Where(m => m.Created <= Convert.ToDateTime(queryCreated));
            }

            //最後的結果
            var list = query.ToList();
        }

問題貌似是可以解決,但是這種用 if 疊加的方式不覺得太不靈活了嗎? 如果客戶心情不好,又來了一個 TradeID between 1 and 10 的篩選條件,那上面的程式碼是不是還得加一個 TradeID 的判斷 ? 太麻煩了,還得繼續尋找更靈活的姿勢。

2. 使用DataTable

哈哈,大家看到 DataTable 是不是有一點懵逼,可不要小瞧這玩意,人家可是直接支援 sql 查詢的哦,這靈活性不容小覷哈,上一段程式碼說話:


        static void Main(string[] args)
        {
            var tradeList = new List<Trade>()
            {
                new Trade(){TradeID=1, TradeTitle="交易1", Created=Convert.ToDateTime("2020/8/1"), CustomerID=1},
                new Trade(){TradeID=2, TradeTitle="交易2", Created=Convert.ToDateTime("2020/8/5"),CustomerID=1},
                new Trade(){TradeID=3, TradeTitle="交易3", Created=Convert.ToDateTime("2020/8/10"), CustomerID=3}
            };

            var table = CopyToDataTable(tradeList);

            var query = table.Select("CustomerID in (1,2,10) and Created <= '2020-08-01' and TradeID >= 1 and TradeID <= 10")
                            .Select(m => new Trade()
                            {
                                TradeID = Convert.ToInt32(m[0]),
                                TradeTitle = Convert.ToString(m[1]),
                                Created = Convert.ToDateTime(m[2]),
                                CustomerID = Convert.ToInt32(3)
                            }).ToList();
        }

        public static DataTable CopyToDataTable<T>(IEnumerable<T> array)
        {
            var ret = new DataTable();
            foreach (PropertyDescriptor dp in TypeDescriptor.GetProperties(typeof(T)))
                ret.Columns.Add(dp.Name);
            foreach (T item in array)
            {
                var Row = ret.NewRow();
                foreach (PropertyDescriptor dp in TypeDescriptor.GetProperties(typeof(T)))
                    Row[dp.Name] = dp.GetValue(item);
                ret.Rows.Add(Row);
            }
            return ret;
        }

是不是很強大,直接將文字化的 sql 塞入到 DataTable 中,你想什麼樣的查詢你就寫什麼樣的 sql 就 ok 啦,當然,理論歸理論,在我的場景中肯定是不會這麼玩的,畢竟記憶體中的 trade 有上千萬行,轉成 DataTable 不是給自己挖坑嘛,那有沒有其他的方式呢?

3. 使用 表示式樹 (ExpressionTree)

我想很多人看到 表示式樹 都會退避三舍,雖然這玩意很強大,但是太複雜了,它會將你的查詢語句拆解成樹中的節點從而構建一棵非常複雜的樹結構,其實 DataTable 對 sql語句的解析也是在記憶體中構建了一棵解析樹,所以這玩意太反人類了,比如你要構建 i > 5 的查詢,你需要下面這樣的硬編碼,這還是非常簡單的哈,複雜的會讓你吐血。

            ParameterExpression param = Expression.Parameter(typeof(int), "i");
            ConstantExpression constExp = Expression.Constant(5, typeof(int));
            BinaryExpression greaterThan = Expression.GreaterThan(param, constExp);
            Expression<Func<int, bool>> f = Expression.Lambda<Func<int, bool>>(greaterThan, param);
            Func<int, bool> mydelegate = f.Compile();
            Console.WriteLine(mydelegate(5));

從圖中可以看到,5>5 = False 是沒有問題的,既然表示式樹是可以解決類似這樣的場景,聰明的你應該會想到,開源社群是否又類似封裝好的 ExpressionTree 開發包呢? 說實話,還真有。。。

4. DynamicExpresso 開發工具包

開源大法好,github地址:https://github.com/davideicardi/DynamicExpresso , 這玩意實現了 將文字化的 C# 語句 動態轉換成 delegate,這句話是什麼意思呢? 大家可以看一下這張圖:

從上圖可以看到,你可以 寫一些文字化的 C# 語句,然後經過 DynamicExpresso 處理後轉換成了可執行 delegate,如果你沒看懂,我用程式碼表示一下,如下圖:

其中: 30 = 5 * 8 / 2 + 10 ,重點在於這裡的 數學表示式 是文字的,有了這個思路,那我是不是也可以將 tradeList 的查詢條件文字化表示,如下程式碼:


            var interpreter = new Interpreter();

            string whereExpression = "(trade.CustomerID == 1 || trade.CustomerID==2 || trade.CustomerID==10) && " +
                                     "trade.Created <= Convert.ToDateTime(\"2020-08-01\") &&" +
                                     "trade.TradeID >= 1 && " +
                                     "trade.TradeID <=10";

            Func<Trade, bool> queryFunc = interpreter.ParseAsDelegate<Func<Trade, bool>>(whereExpression, "trade");

            var list = tradeList.Where(queryFunc).ToList();

問題搞定,還是比較完美的 ???

三: 總結

總的來說,有了DynamicExpresso ,我就可以將 文字化的 C#語句 直接丟給 Where 條件就可以靈活檢索,完美的解決了在記憶體中查詢 tradelist 資料分佈情況,當然目前的 DynamicExpresso 還有很多語句不支援,不過都在完善中,期待大家支援點贊加貢獻。

如您有更多問題與我互動,掃描下方進來吧~

圖片名稱

相關文章