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