EF6を使っていてタイトルの事をやろうとしてしばらくハマったのでメモです。
やりたいこと
EmpテーブルのATTR01~ATTR10という列を、List(Of String)に変換したいです。
みなさんも、下記のような連番が振られた非正規化フィールドをリストや配列にマッピングしたいと思ったことはありませんか?
eid | EmpName | ATTR01 | ATTR02 | ATTR03 | ... | ATTR10 |
---|---|---|---|---|---|---|
AAA | hoge | A | B | C | ... | J |
Class EmpViewModel
Public Property eid As String
Public Property EmpName As String
Public Property Attr As New List(Of String)
End Class
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を文字列配列として渡すようにして初期化しました。
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()
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初期化子を使って行っていたので、そこについては問題が起きていませんでしたが、もしここが次のようになっていたら、同じようにエラーが起きていたでしょう。
Select New EmpViewModel( hoge, ... )
select new EmpViewModel( hoge, ... )
対処方法1
コンストラクタ付き引数でリストを初期化するのではなく、From初期化子を使うようにします。
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()
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に変換されているんだろう」と興味本位でチェックをしてみました。
db.Database.Log = Sub(sql) Debug.WriteLine(sql) 'ログをDebug出力に出力する
db.Database.Log = sql => Debug.WriteLine(sql); // ログをDebug出力に出力する
すると、詳細は省略しますが、こんな感じの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]
:
これはさすがに気になります。
そもそもを考えると、今回やりたいのは、
- クエリ式でDBからデータを取得したい
- エンティティモデルに結果をマッピングしたい
の2つであり、それを1つのクエリ式でいっぺんにやっていたのが問題ではないでしょうか。
対処方法2
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()
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との境界」を意識せざるを得ないのは、ちょっと使いにくいですね…。とはいえ便利な機能なので、癖を理解しつつ使っていきたいと思います。