顯示具有 DTO 標籤的文章。 顯示所有文章
顯示具有 DTO 標籤的文章。 顯示所有文章

2020年12月12日 星期六

[Applied]LINQ版本的視窗函數(SQL Window Function)

資料庫SQL中有一些配合Over字句Patition By群組後去處理集合
相當好用的視窗函數,例如編號用的Row_Number、Rank、Dense_Rank等等
似乎可以在LINQ中實現

因此在新版本的Applied套件中新增了此項功能

Nuget網址: https://www.nuget.org/packages/Applied/

底下為示範的程式

public class TestData
{
    public int Year { get; set; }
    public string Name { get; set; }
    public decimal Value { get; set; }
    public decimal Sum { get; set; }
    public decimal Average { get; set; }
    public int RowNumber { get; set; }
    public int Ntile { get; set; }
    public int DenseRank { get; set; }
    public int Rank { get; set; }
    public decimal FirstValue { get; set; }
    public decimal LastValue { get; set; }
    public decimal NthValue { get; set; }
    public decimal Lead { get; set; }
    public decimal Lag { get; set; }
    public decimal CumeDist { get; set; }
    public decimal PercentRank { get; set; }
    public decimal PercentileDisc { get; set; }
    public decimal PercentileCont { get; set; }
    public decimal KeepDenseRankFirst { get; set; }
    public decimal KeepDenseRankLast { get; set; }
    public decimal TeaTime { get; set; }
}
List<TestData> data = new List<TestData>();
data.Add(new TestData() { Year = 2019, Name = "A", Value = 111.1m });
data.Add(new TestData() { Year = 2019, Name = "B", Value = 333.3m });
data.Add(new TestData() { Year = 2019, Name = "C", Value = 333.3m });
data.Add(new TestData() { Year = 2019, Name = "A", Value = 222.2m });
data.Add(new TestData() { Year = 2019, Name = "C", Value = 444.4m });
data.Add(new TestData() { Year = 2019, Name = "A", Value = 222.2m });
data.Add(new TestData() { Year = 2019, Name = "B", Value = 333.3m });
data.Add(new TestData() { Year = 2019, Name = "C", Value = 555.5m });
data.Add(new TestData() { Year = 2020, Name = "A", Value = 111.1m });
data.Add(new TestData() { Year = 2020, Name = "B", Value = 333.3m });
data.Add(new TestData() { Year = 2020, Name = "A", Value = 222.2m });
data.Add(new TestData() { Year = 2020, Name = "C", Value = 333.3m });

data = data.GroupBy(a => new { a.Year }).AsPartition(p => p.OrderBy(a => a.Value).ThenBy(a => a.Name))
.Over(p => p.Sum(a => a.Value), (a, value) => a.Apply(() => new { Sum = value }))
.Over(p => p.Average(a => a.Value), (a, value) => a.Apply(() => new { Average = value }))
.Over(p => p.RowNumber(), (a, value) => a.Apply(() => new { RowNumber = value }))
.Over(p => p.Ntile(2), (a, value) => a.Apply(() => new { Ntile = value }))
.Over(p => p.DenseRank(), (a, value) => a.Apply(() => new { DenseRank = value }))
.Over(p => p.Rank(), (a, value) => a.Apply(() => new { Rank = value }))
.Over(p => p.FirstValue(a => a.Value), (a, value) => a.Apply(() => new { FirstValue = value }))
.Over(p => p.LastValue(a => a.Value), (a, value) => a.Apply(() => new { LastValue = value }))
.Over(p => p.NthValue(a => a.Value, 2), (a, value) => a.Apply(() => new { NthValue = value }))
.Over(p => p.Lead(a => a.Value), (a, value) => a.Apply(() => new { Lead = value }))
.Over(p => p.Lag(a => a.Value), (a, value) => a.Apply(() => new { Lag = value }))
.Over(p => p.CumeDist(), (a, value) => a.Apply(() => new { CumeDist = value }))
.Over(p => p.PercentRank(), (a, value) => a.Apply(() => new { PercentRank = value }))
.Over(p => p.PercentileDisc(0.5m, a => a.Value), (a, value) => a.Apply(() => new { PercentileDisc = value }))
.Over(p => p.PercentileCont(0.5m, a => a.Value), (a, value) => a.Apply(() => new { PercentileCont = value }))
.Over(p => p.KeepDenseRankFirst(g => g.Sum(a => a.Value)), (a, value) => a.Apply(() => new { KeepDenseRankFirst = value }))
.Over(p => p.KeepDenseRankLast(g => g.Sum(a => a.Value)), (a, value) => a.Apply(() => new { KeepDenseRankLast = value }))
.ToList();

執行結果


 

其中Over方法的第一個Func參數有兩種型態

一個是直接由IEnumerable<TSource>集合回傳單一值IElement的彙總方法
可以使用Linq原有的Sum、Average等等去計算集合部分的結果

另一個是IWindowFunctionFactory<TSource>
要回傳IWindowFunction<TSourceBase, IElement>的視窗函數物件,由該物件執行計算

目前已實現有15種一般的Window Function
不過也許可能有特殊需求情況,需要自行去實現自訂的Window Function
可以用IWindowFunction<TSourceBase, IElement>介面去達到自訂功能,如下

public class TeaTime<TSourceBase, IElement> : IWindowFunction<TSourceBase, IElement>
{
    private readonly Func<TSourceBase, IElement> _field;
    public TeaTime(Func<TSourceBase, IElement> field)
    {
        if (field == null)
        {
            throw new ArgumentNullException("field");
        }
        _field = field;
    }
    public IEnumerable<TResult> GetPartitionResults<TSource, TResult>(IRankEnumerable<TSource> elements
        , Func<TSource, IElement, TResult> selector) where TSource : TSourceBase
    {
        foreach (TSource element in elements)
        {
            IElement value = _field(element);
            yield return selector(element, value);
        }
    }
}
public static class MyWindowFunctions
{
    public static IWindowFunction<TSource, IElement> TeaTime<TSource, IElement>(this IWindowFunctionFactory<TSource> factory
        , Func<TSource, IElement> field)
    {
        return new TeaTime<TSource, IElement>(field);
    }
}
data = data.GroupBy(a => new { a.Year }).AsPartition(p => p.OrderBy(a => a.Value).ThenBy(a => a.Name))
.Over(p => p.TeaTime(a => a.Value), (a, value) => a.Apply(() => new { TeaTime = value }))
.ToList();

 

2017年5月31日 星期三

[Applied]簡易的資料對映轉換套件



Applied是一個用於處理DTO屬性值對映複製的.NET元件

概念來自於T-SQL的CROSS APPLY,所以擴充函式才會使用Lambda型態的參數做設計


Nuget網址: https://www.nuget.org/packages/Applied/


使用上非常簡單,首先宣告示範程式用的資料物件類別
    public enum UserEnum
    {
        None,
        User
    }
    [Serializable]
    public class User
    {
        public int UserID { get; set; }
        public string Name { get; set; }
        public DateTime? Time { get; set; }
        public UserEnum Enum { get; set; }
    }
    [Serializable]
    public class UserViewModel
    {
        public int UserID { get; set; }
        public string Name { get; set; }
        public DateTime? Time { get; set; }
        public UserEnum Enum { get; set; }
    }
然後建立一個陣列並對其使用Applied提供的擴充函式
    User[] users = new User[]
    {
        new User() { UserID = 1, Name = "Sam     " },
        new User() { UserID = 2, Name = "John    " }
    };

    users.Apply(a => new { Time = DateTime.Now, Enum = UserEnum.User });
    users.Trim();

這裡Apply()會令陣列的所有項目屬性與參數中給的賦值物件的屬性值設為一樣

只要兩者的屬性名稱相同,型別相同或型別能相互轉換的

Trim()則會除去所有字串屬性值的前後空白

因為函式其實有回傳自身所以也能夠使用類似裝飾者的寫法或者用於Linq查詢語法內
    users.Apply(a => new { Time = DateTime.Now, Enum = UserEnum.User }).Trim();
對於一般方式來說此行的程式則大約需要寫成這樣
    for (int i = 0; i < users.Length; i++)
    {
        users[i].Time = DateTime.Now;
        users[i].Enum = UserEnum.User;
        if (users[i].Name != null)
        {
            users[i].Name = users[i].Name.Trim();
        }
    }
如果每個物件需要設定的屬性越多型別轉換越多寫起來就會越麻煩複雜


而且除了有設定好資料類別的資料物件可以使用這些函式以外

DataTable,DataRow,IDictionary(Key為string)等型態的物件也可以使用這些函式設定或作為賦值的參數

另外還有幾個相互轉換用的函式
    UserViewModel[] vm1 = users.ToDataEnumerable<User, UserViewModel>().ToArray();
    DataTable dt1 = users.ToDataTable();
    Dictionary<string, object>[] ds1 = users.ToDictionaries().ToArray();

    UserViewModel[] vm2 = dt1.ToDataEnumerable<UserViewModel>().ToArray();
    UserViewModel[] vm3 = ds1.ToDataEnumerable<UserViewModel>().ToArray();

    DataTable dt2 = vm1.ToDataTable();
    DataTable dt3 = ds1.ToDataTable();

    Dictionary<string, object>[] ds2 = vm1.ToDictionaries().ToArray();
    Dictionary<string, object>[] ds3 = dt1.ToDictionaries().ToArray();


資料類別陣列,DataTable,Dictionary陣列皆可互相轉換


然後是有關效能的部分

Applied的對映功能主要是以TypeDescriptor的方式來達成
沒有需要程式先預初始化的設計,所以是每次呼叫都會自動的做一次Mapping動作
不過其中還是具有依靠陣列長度來判斷是否轉換為使用Expression Tree以加速處理的機制
(Expression Tree初始較慢執行較快,效益臨界值是訂672,以Array或List使用才能判斷)
固每次任務皆少筆數的話整體效能大致上應該不高,不過差別大概也感覺不到(筆數少)
就看請求任務的總次數多或不多來評估適不適合使用了



新版改以Expression Tree為主,在沒有需要程式先預初始化的設計情況

第一次自動Mapping後存取屬性的Lambda會存入記憶體的字典中下次呼叫就能以類別來快取
https://dotblogs.com.tw/initials/2016/08/20/231753

與這篇文章相同的百萬筆測試,結果為700~800左右,原生的14~17倍

結果為400~500,原生的10倍左右


以上請參考,若有錯誤煩請告知,感謝~