[Entity Framework]テーブル列01～10を配列にマッピングする

EF6を使っていてタイトルの事をやろうとしてしばらくハマったのでメモです。

# やりたいこと
EmpテーブルのATTR01～ATTR10という列を、List(Of String)に変換したいです。
みなさんも、下記のような連番が振られた非正規化フィールドをリストや配列にマッピングしたいと思ったことはありませんか？

| eid | EmpName | ATTR01 | ATTR02 | ATTR03 | ... | ATTR10 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| AAA | hoge | A | B | C | ... | J |

```VB:VB
Class EmpViewModel
	Public Property eid As String
	Public Property EmpName As String
	Public Property Attr As New List(Of String)
End Class
```

```C#:C#
class EmpViewModel
{
    public string eid { get; set; }
    public string EmpName { get; set; }
    public List<string> Attr {get; set; } = new();
}
```

そして、その変換（マッピング）を、LINQ to Entities のクエリ式にてスマートに行いたいのです。

# 問題が起こったやり方

以下のように、クエリ式を用いてレコードの絞り込み結果をSelect句でEmpViewModelにマッピングしました。
その際に、List(Of String)のコンストラクタにATTR01～ATTR10を文字列配列として渡すようにして初期化しました。

```vb:VB
Dim query_result = (
    From e In db.Emp
    Where e.eid = id
    Select New EmpViewModel With {
        .eid = e.eid,
        .EmpName = e.EmpName,
        .Attr = New List(Of String)({
            emp.ATTR01,
            emp.ATTR02,
            emp.ATTR03,
            emp.ATTR04,
            emp.ATTR05,
            emp.ATTR06,
            emp.ATTR07,
            emp.ATTR08,
            emp.ATTR09,
            emp.ATTR10
        })
    }
).ToList()
```

```C#:C#
var query_result = (
    from e in db.Emp
    where e.eid == id
    select new EmpViewModel() {
        eid = e.eid,
        EmpName = e.EmpName,
        Attr = new List<string>(new[]{
            emp.ATTR01,
            emp.ATTR02,
            emp.ATTR03,
            emp.ATTR04,
            emp.ATTR05,
            emp.ATTR06,
            emp.ATTR07,
            emp.ATTR08,
            emp.ATTR09,
            emp.ATTR10
        })
    }
).ToList();
```

# 起こった問題

コンパイルは通るのですが、下記の実行時エラーが出ました。

> Only parameterless constructors and initializers are supported in LINQ to Entities.
> LINQ to Entities ではパラメーターなしのコンストラクターおよび初期​化子のみがサポートされます。

# 原因
- .Attrプロパティへのマッピング時に「パラメータありのコンストラクタ（ここでは、初期化用の文字列配列を受け取るコンストラクタ）」を使っている。
- LINQ to Objects（In-memoryなデータを扱うLINQ）ならば問題はないが、これはLINQ to Entitiesであり、IQueryableな世界なので、最終的にSQLに変換されなければならない。
- SQLの世界には引数付きコンストラクタは存在しないのでエラーになっている。

上記ではたまたまEmpViewModelの生成をWith初期化子を使って行っていたので、そこについては問題が起きていませんでしたが、もしここが次のようになっていたら、同じようにエラーが起きていたでしょう。

```vb:VB
	Select New EmpViewModel( hoge, ... )
```

```C#:C#
    select new EmpViewModel( hoge, ... )
```

# 対処方法1
コンストラクタ付き引数でリストを初期化するのではなく、From初期化子を使うようにします。

```vb
Dim query_result = (
    From e In db.Emp
    Where e.eid = id
    Select New EmpViewModel With {
        .eid = e.eid,
        .EmpName = e.EmpName,
        .Attr = New List(Of String)() From {
            emp.ATTR01,
            emp.ATTR02,
            emp.ATTR03,
            emp.ATTR04,
            emp.ATTR05,
            emp.ATTR06,
            emp.ATTR07,
            emp.ATTR08,
            emp.ATTR09,
            emp.ATTR10
        }
    }
).AsEnumerable()
```

```C#:C#
var query_result = (
    from e in db.Emp
    where e.eid == id
    select new EmpViewModel() {
        eid = e.eid,
        EmpName = e.EmpName,
        Attr = new List<string>(){
            emp.ATTR01,
            emp.ATTR02,
            emp.ATTR03,
            emp.ATTR04,
            emp.ATTR05,
            emp.ATTR06,
            emp.ATTR07,
            emp.ATTR08,
            emp.ATTR09,
            emp.ATTR10
        }
    }
).AsEnumerable();
```

これで実行時エラーは出なくなり、結果が取得できます。
しかし、「これってどんなSQLに変換されているんだろう」と興味本位でチェックをしてみました。

```vb:発行されるSQLをチェックする為のコード(VB)
db.Database.Log = Sub(sql) Debug.WriteLine(sql)	'ログをDebug出力に出力する
```

```C#:発行されるSQLをチェックする為のコード(C#)
db.Database.Log = sql => Debug.WriteLine(sql); // ログをDebug出力に出力する
```

すると、詳細は省略しますが、こんな感じのSQLが発行されていたのです。

```SQL:(一部抜粋)
[UnionAll9].[C1] AS [C1], 
CASE WHEN ([UnionAll9].[C1] = 0) THEN [Extent2].[ATTR01] WHEN ([UnionAll9].[C1] = 1) THEN [Extent2].[ATTR02] WHEN ([UnionAll9].[C1] = 2) THEN [Extent2].[ATTR03] WHEN ([UnionAll9].[C1] = 3) THEN [Extent2].[ATTR04] WHEN ([UnionAll9].[C1] = 4) THEN [Extent2].[ATTR05] WHEN ([UnionAll9].[C1] = 5) THEN [Extent2].[ATTR06] WHEN ([UnionAll9].[C1] = 6) THEN [Extent2].[ATTR07] WHEN ([UnionAll9].[C1] = 7) THEN [Extent2].[ATTR08] WHEN ([UnionAll9].[C1] = 8) THEN [Extent2].[ATTR09] ELSE [Extent2].[ATTR10] END AS [C2], 
1 AS [C3]
 :
CROSS JOIN  (SELECT [c].[C1] AS [C1]
    FROM  (SELECT [c].[C1] AS [C1]
        FROM  (SELECT [c].[C1] AS [C1]
            FROM  (SELECT [c].[C1] AS [C1]
                FROM  (SELECT [c].[C1] AS [C1]
                    FROM  (SELECT [c].[C1] AS [C1]
                        FROM  (SELECT [c].[C1] AS [C1]
                            FROM  (SELECT [c].[C1] AS [C1]
                                FROM  (SELECT 
                                    0 AS [C1]
                                    FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]
                                UNION ALL
                                    SELECT 
                                    1 AS [C1]
                                    FROM  ( SELECT 1 AS X ) AS [SingleRowTable2]) AS [c]
                            UNION ALL
                                SELECT 
                                2 AS [C1]
                                FROM  ( SELECT 1 AS X ) AS [SingleRowTable3]) AS [c]
                        UNION ALL
                            SELECT 
                            3 AS [C1]
                            FROM  ( SELECT 1 AS X ) AS [SingleRowTable4]) AS [c]
                    UNION ALL
                        SELECT 
                        4 AS [C1]
                        FROM  ( SELECT 1 AS X ) AS [SingleRowTable5]) AS [c]
                UNION ALL
                    SELECT 
                    5 AS [C1]
                    FROM  ( SELECT 1 AS X ) AS [SingleRowTable6]) AS [c]
            UNION ALL
                SELECT 
                6 AS [C1]
                FROM  ( SELECT 1 AS X ) AS [SingleRowTable7]) AS [c]
        UNION ALL
            SELECT 
            7 AS [C1]
            FROM  ( SELECT 1 AS X ) AS [SingleRowTable8]) AS [c]
    UNION ALL
        SELECT 
        8 AS [C1]
        FROM  ( SELECT 1 AS X ) AS [SingleRowTable9]) AS [c]
UNION ALL
    SELECT 
    9 AS [C1]
    FROM  ( SELECT 1 AS X ) AS [SingleRowTable10]) AS [UnionAll9]
  :
```

これはさすがに気になります。

そもそもを考えると、今回やりたいのは、

1. クエリ式でDBからデータを取得したい
2. エンティティモデルに結果をマッピングしたい

の２つであり、それを1つのクエリ式でいっぺんにやっていたのが問題ではないでしょうか。

# 対処方法2

```vb:VB
Dim query_result = (
    From e In db.Emp
    Where e.eid = id
    ).AsEnumerable()	'AsEnumerable()で一旦クエリを解決し、SQLを実行

Dim list = (
	From e In query_result
    Select New EmpViewModel With {
        .eid = e.eid,
        .EmpName = e.EmpName,
        .Attr = New List(Of String)() From {
            emp.ATTR01,
            emp.ATTR02,
            emp.ATTR03,
            emp.ATTR04,
            emp.ATTR05,
            emp.ATTR06,
            emp.ATTR07,
            emp.ATTR08,
            emp.ATTR09,
            emp.ATTR10
        }
    }
).AsEnumerable()
```

```C#:C#
var query_result = (
    from e in db.Emp
    where e.eid == id
    ).AsEnumerable();	// AsEnumerable()で一旦クエリを解決し、SQLを実行

var list = (
	from e in query_result
    select new EmpViewModel(){
        eid = e.eid,
        EmpName = e.EmpName,
        Attr = new List<string>(){
            emp.ATTR01,
            emp.ATTR02,
            emp.ATTR03,
            emp.ATTR04,
            emp.ATTR05,
            emp.ATTR06,
            emp.ATTR07,
            emp.ATTR08,
            emp.ATTR09,
            emp.ATTR10
        }
    }
).AsEnumerable();
```


これで、発行されるSQLはごくシンプルなものになります。
また、listを生成する為のクエリ式の中でもしパラメータありコンストラクタを使用しても実行時エラーにならなくなります（そこは既にLINQ to Objectsの世界だからです）。

しかし、本来DBを意識しなくても良いのがLINQ to Entitiesの利点なのに、こうやって「DBとの境界」を意識せざるを得ないのは、ちょっと使いにくいですね…。とはいえ便利な機能なので、癖を理解しつつ使っていきたいと思います。