2009年11月29日日曜日

LinqToSqlでの開発の注意点 その2

今回はLinqToSqlを使用してDBアクセスする際の注意点を解説する。

LinqToSqlで開発する場合、DBMLファイルを使用しDBMLで生成されたDataContextを使用するのが常道だろう。その際に、LinqToSqlを介しDBのテーブルにマッピングされたクラスを使用していると、ともするとメモリ上のデータを扱っているような錯覚を起こす場合がある。そのため経験の浅いプログラマはパフォーマンスを考慮せずに実装したりするのだが、実際にCo-opの学生が実装したコードを次に紹介しよう。

using( var db = new HogeDataContext())
{
 foreach( var user in db.Users.Where(x=> x.Hoge))
 {
  DoSomething(user);
  var userClients = user.UserClients.Select( x => new SimpleUserClient(){ Name=x.Name, Address=x.Address });
  DoSomethingWithClients(userClients);
 }
}

上記の例は実際に彼が実装していたのとは異なるのだが、やっているコンセプトは同じだ。もちろん上記のコードで期待した動作はできるのだがDBのパフォーマンス上よろしくない。

その理由は上記のコードをSqlに展開するとよく分かる。展開したSqlが下記になる。

select * from Users where Hoge=1
select Name, Address from UserClients where UserId=1
select Name, Address from UserClients where UserId=2
select Name, Address from UserClients where UserId=n




という風に2行目以降は1行目で取得したUser分だけ生成、実行されることになる。仮にUserを100人分取得した場合は100回DBにアクセスすることになる。SqlServer 2008とともにインストールされているSql Server Profilerを起動してから上記のコードを実行すると実際にSqlServer上で実行されているSqlが参照できるので良く分かると思う。

実際にはこのように書くべきだ。

using( var db = new HogeDataContext())
{
 foreach( var user in db.Users.Where(x=> x.Hoge).Select(x=> new { User=x, Clients=x.Clients.Select(x=>new SimpleUserClient(){ Name=x.Name, Address=x.Address }) })
 {
  DoSomething(user.User);
  DoSomethingWithClients(user.Clients);
 }
}

これでSqlの実行は一度だけになりDBのリソースを無駄に消費することはなくなった。最後に上記のコードから生成されるSqlを紹介しておこう。(実際に生成されたSqlの不要な部分を削ったりしてあります)

SELECT [t0].[UserId], [t0].[UserName], [t1].[Name], [t1].[Address](
SELECT COUNT(*)
FROM [dbo].[UserClients] AS [t2]
WHERE [t2].[UserId] = [t0].[UserId]
) AS [value]
FROM [dbo].[Users] AS [t0]
LEFT OUTER JOIN [dbo].[UserClients] AS [t1] ON [t1].[UserId] = [t0].[UserId]
ORDER BY [t0].[UserId], [t1].[Id]

Co-opの彼が担当した部分に今回紹介したようなコードが大量に散見されたために卒倒しそうになったので、今回紹介したようなコードを安易に大量生成するのはやめよう。

0 件のコメント:

コメントを投稿