LINQ を使ってデータベースアクセスする時、良く分からず Include()
を書いていませんか?
データベース観点で言う、複数のテーブルからデータを取りたいときに必要な処理ですが、今回はその辺りを解説してみたいと思います。
テーブルの結合
このお話をする前に、「テーブルの結合」について理解する必要があります。
前職でテーブルの結合を理解していない頃に大変な目に遭いましたので、同じような犠牲者が出ないためにも改めて書きたいと思います。
※ ご存知の方は読み飛ばしていただけたらと思います
データベースにデータを保管するシステムの場合、大抵は複数のテーブルにデータを保管しています。
例えば「ユーザ情報」とか、「組織情報」とかに分かれています。
しかし、プログラムからデータベースのデータを検索する時、欲しいデータが複数のテーブルに散らばっていることがよくあります。
例えば「ユーザ情報一覧」みたいな画面を作る時、「ユーザの名前」と「所属する組織の名前」を横並びで出す仕様になったとすると、「ユーザの名前」は「ユーザ情報」テーブルに、「組織の名前」は「組織情報」テーブルに入っています。
こんな感じでしょうか。
ユーザ情報テーブル
ユーザID | ユーザ名 | 所属組織ID |
1 | 山田太郎 | 1 |
2 | 田中次郎 | 2 |
3 | 鈴木三郎 | 2 |
組織情報テーブル
組織ID | 組織名 |
1 | 営業部 |
2 | 開発部 |
サンプルデータのセンスが昭和なのは置いておいて…このテーブルからデータを取ってくるとき、どのような SQL を書きますか?
テーブルの結合をご存じない場合であれば、例えば、
SELECT ユーザ名, 所属組織ID FROM ユーザ情報テーブル
と実行してユーザ名と所属組織IDの一覧を取ってきて、その後、所属組織IDを使って再度
SELECT 組織名 FROM 組織情報テーブル
WHERE 組織ID = '<さっき取得した所属組織ID>'
みたいに順番に所属組織を取ってくれば良いですかね?
そうすると、サンプルデータの場合、下記の回数のSQLを実行することになります。
- ユーザ情報の取得 × 1
- 組織情報の取得 × 3
今回はユーザ数が 3
だったので大したことは無いですが、ユーザ数が 100
だったらどうしましょう?ユーザ数が 100000
だったらどうしましょう?
ものすごい数の SQL を実行しないといけません。非常に非効率ですよね?
本来はどうするべきでしょうか?
テーブルの結合をする JOIN句 を使用するのがスマートです。
詳しくは調べてみてください、なのですが、今回は INNER JOIN を使用すべきケースで、下記のSQLを使用することになります。
SELECT U.ユーザ名, O.組織名 FROM ユーザ情報 U
INNER JOIN 組織情報 O ON U.所属組織ID = O.組織ID
これで、一度の SQL で複数のテーブルの情報を結合して取得することができました。
C# 観点で見るテーブル結合
C# でも LINQ to Entities を使えばデータベースアクセスが可能です。
そして外部キーも定義できます。
今回の例を Model にするなら、下記のような記載です。
/// <summary>
/// 組織
/// </summary>
public class Organization
{
/// <summary>
/// 組織ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 組織名
/// </summary>
public string Name { get; set; }
}
/// <summary>
/// ユーザ情報
/// </summary>
public class UserInfo
{
/// <summary>
/// ユーザID
/// </summary>
public int Id { get; set; }
/// <summary>
/// ユーザ名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 組織ID
/// </summary>
public int OrganizationId { get; set; }
/// <summary>
/// 組織
/// </summary>
public Organization Organization { get; set; }
}
組織情報は普通にプロパティを持っているだけのデータ構造ですが、ユーザ情報には組織情報への参照 (Organization
プロパティ) が含まれています。
こうすることで、ユーザ情報のオブジェクトを経由して、そのオブジェクトに関係している組織情報を簡単に取り出せそうに思います。
具体的に言うと、下記の記述でユーザ情報に関連する組織情報が取れそうです。
var infoes = _context.UserInfoes.ToList();
foreach (var user in infoes)
{
var orgName = user.Organization.Name;
}
ただ、こう書いて実行してしまうと、user.Organizationがnullです
とエラーになります。
C# 的には間違ったことを書いていないように見えますが、組織情報を取ってきてはくれません。
※ ちなみに .NET Framework (正確には Entity Framework 6 ) を使用している場合、デフォルトの構成だと組織情報取ってくると思われます
どうすればよいのか
上記の処理を実行したときの出力ウィンドウには、下記の SQL が出ています。
SELECT [u].[Id], [u].[Name], [u].[OrganizationId]
FROM [UserInfoes] AS [u]
つまり、ユーザ情報テーブルしか参照できていません。
クラス内で他のテーブルへ参照する状態を持っていても、その対象に明示的に取りに行こうとしない限りは取ってくれないのです。
どうすればよいかというと、先ほどのコードを下記のように修正します。
var infoes = _context.UserInfoes.Include(u => u.Organization).ToList();
foreach (var user in infoes)
{
var orgName = user.Organization.Name;
}
ToList()
する前に Include()
文を挟みました。
これにより、SQL文が下記のように改善され、組織情報を取ってくるようになりました。
SELECT [u].[Id], [u].[Name], [u].[OrganizationId], [u.Organization].[Id], [u.Organization].[Name]
FROM [UserInfoes] AS [u]
INNER JOIN [Organizations] AS [u.Organization] ON [u].[OrganizationId] = [u.Organization].[Id]
ということで、Include()
文は、「このデータもテーブル結合して持ってきてね」という指示であると覚えておくと良いです。
こちらは前回解説した遅延実行の一つですので、必ず ToList()
などの実際にデータベースアクセスが発生する処理の前に記載する必要があります。ご注意ください。