次の方法で共有


働くプログラマ

Oak 入門: データベース操作

Ted Neward

お帰りなさい。ここ数回のこのコラムでは、Oak 入門を取り上げてきました。Oak は、C# 言語、ASP.NET MVC フレームワークなどの慣れ親しんだ Microsoft .NET Framework を使い続けながらも Ruby や Node.js の世界のアイデアを取り込んだ、Web 開発の動的アプローチです。プロジェクトの作者である Amir Rajan の言葉を借りると、Oak の本質は "迅速なフィードバック、ストレスのない開発、および形式ばらないコード" です。

前回は、データベースに接続できないという Oak の通知が表示されるところまで進みました。今回の内容は、Oak を SQL Server インスタンスに接続し、システムに別の関連する型 (ブログ エントリのコメント) を追加して、Oak でデータベースを構築してから、Oak を使用して 2 つの型を関連付ける方法についてです。また、Oak と Oak のビルド システムでデータベース操作を制御するしくみについても説明します。

前回のあらすじ

Oak を前回実行した際は、エラー メッセージと役に立つ説明文が表示されました。その抜粋を図 1 に示します (前回の記事 msdn.microsoft.com/magazine/dn532208に沿って作業する場合は、サーバーと sidekick ランナーを起動する必要があることをお忘れなく)。先に進む前に留意していただきたいのですが、Oak では開発者が問題の原因を発見または調査できる例外スタック トレースが表示されるだけでなく、実際に問題の診断が試行され、解決策が表示されます。この例では、"Update the web config with your server instance" (サーバー インスタンスで Web 構成を更新してください) という解決策が表示されています。


図 1 エラーがスローされた後の Oak プロジェクトのヘルプ ウィンドウ

予想どおり、"warmup" gem から取り出した web.config ファイルをざっと確認すると、"(local)" データ ソースを参照するシンプルなプレースホルダーが見つかります。このデータ ソースは、ローカル SQL Server インスタンスの構成方法によっては構成に大きな影響を及ぼすことがあります。とは言え、LocalDB 接続文字列の使用もリモート SQL Server 文字列 (または、クラウドにデータを保存する場合は Windows Azure SQL データベース) の使用も、同じくらい簡単です。個人的には、LocalDB の使用が好みです。軽量で非常に簡単にクリーンアップできるからです。今回のような調査の場合、通常は "Server=(localdb)\v11.0;Integrated Security=true" という単純な接続文字列になります。どの SQL 接続を使用する場合も、// 要素に接続を指定してページを更新するだけです。実は、Oak ではコードベース全体でこのような処理を実行するために "rake update_db_server[(localdb)\\v11.0]" (バックスラッシュをエスケープする必要があります) という rake (Ruby のビルド ツール) タスクを利用できます。

ヒューストン、応答せよ

残念ながら、Oak を再度実行しても、何も変更しなかったかのように Oakが動作します。やはり残念ながら、これはまったくそのとおりです。sidekick の監視対象は、web.config への変更ではなくソース ファイルへの変更のみのようです。web.config ファイルを変更することがほとんどない点を考えると、これは致命的な欠陥ではありませんが、かなり簡単に済むはずの作業が少し難しくなります。プロジェクトの変更が sidekick に認識されるようにするには、HomeController.cs や別のソース ファイルなど、重要なファイルの保存をトリガーするだけで十分です。この処理には 2 つの方法があります。目的のファイルに移動して、ファイル内のランダムな場所を選択し、Spaceキーを押してから入力したスペースを削除する (この結果、ファイルの変更が Visual Studio に認識され、保存が実行される) 方法と、コマンド プロンプトから rake を手動で起動する方法です。

いずれかの方法を実行すると、ブラウザーが更新され、"Invalid object name 'Blogs'" (オブジェクト名 'Blogs' が無効です) という別のエラーが表示されます。これで、データベース移行の問題に正面から取り組む準備ができました。

データ、データ、データ

概念上の先行テクノロジと同様に、Oak ではデータベースとデータベース スキーマを管理しようとするので、多かれ少なかれデータベースは開発者にとって "隠された" ままになります。この例の場合、Oak ではデータベースがゼロから "自動" で作成されません。なぜなら、開発者によってはスキーマの形式にこだわりがあり、開発者がこだわりを持っていなくてもデータベース管理者は持っていることも多いからです (だれが主導権を握るか、または握るべきかについては、酒の席で、できればプロジェクトの完了からずっとたってから話題にするのが一番です)。

Oak でデータベースをシードするには、SeedController.cs の HomeController のすぐ隣で入れ子になっている、SeedController という特定のコントロールを使用します。SeedController.cs ファイルには既に SeedController の定義が含まれていますが、開発者の目的にとってさらに重要な Schema クラスも含まれています。Schema クラスを使用すれば、スキーマの構築や場合によってはサンプル データの作成に必要な手順が簡単になります。なお、Oak の既定の規則ではオブジェクトを複数形の名前のデータベース テーブルに保存するので、Blog オブジェクトを Blogs という名前のテーブルに保存していることに留意してください。図 2 に、Schema クラスを示します。

図 2 Schema クラス

public class Schema
 {
   // This is the method you'll want to alter
   public IEnumerable> Scripts()
   {
     // Replace all content inside of the Scripts() method with this line
     yield return CreateBlogsTable; // Return just the pointer to the function
   }
   public string CreateBlogsTable() // Here is the function definition
   {
     // This is an example, your table name may be different
     // For more information on schema generation check out the Oak wiki
     return Seed.CreateTable("Blogs",
       Seed.Id(),
       new { Name = "nvarchar(255)" },
       new { Body = "nvarchar(max)" }
     );
   }
   public void SampleEntries()
   {
   }
   public Seed Seed { get; set; }
   public Schema(Seed seed) { Seed = seed; }
 }

Scripts メソッドでの "yield return" の特殊な使い方に注目してください。C# 2.0 当時の動作をご存じない方のために説明すると、"yield return" は、オブジェクトの匿名の "ストリーム" を作成し、このストリームを参照する IEnumerable を返します。今回の例では、これは関数のストリームで、さらに具体的に言えばこれは 1 つの関数 (CreateBlogsTable) のストリームです。基本的には、データベースの作成方法を示す操作 (Func インスタンス) のストリームを作成すると、このストリームから返された操作や関数がそれぞれ Oak に渡されて実行されます。このようにすれば、スキーマが新たに変更されたときに、その変更を新しい関数 (たとえば、"CreateCommentsTable") でキャプチャしてリストに追加できます。必要に応じて、最初の関数を Version1、2 つ目の関数を Version2 のように呼べば、データベース スキーマのバージョン管理が可能です。詳細については、Oak wiki (bit.ly/1bgods5、英語) を参照してください。

(msdn.microsoft.com/magazine/ff955611から始まるこのコラムの「マルチパラダイムと .NET」シリーズを覚えていらっしゃる方が指摘されるとおり、これは、この問題に対するかなり関数指向の対処方法です。)

Seed.Id 関数は、正規主キー列、つまり主キーとしてマークされて自動インクリメントされる整数値を作成します。Oak wiki (bit.ly/1iKfcIb、英語) では、正規主キー列を作成するために Seed クラスを使用してデータベースを作成するリファレンスが公開されていますが、いつでも必要に応じて、SQL をそのまま実行する代わりに次のようなアドホック フォールバックを利用できます。

public IEnumerable AdHocChange()
 {
   var reader = "select * from SampleTable".ExecuteReader();
   while (reader.Read())
   {
     // Do stuff here like yield return strings
   }
     var name = "select top 1 name from sysobjects"
       .ExecuteScalar() as string;
     yield return "drop table SampleTable";
     yield return "drop table AnotherSampleTable";
   }

"yield return" の別の呼び出し先メソッドとして AdHocChange メソッドを追加すれば、これらのコマンドは Oak で問題なく実行されます (先ほどの wiki のリンクが機能しない場合は、Oak で表示されるエラー/ヘルプ メッセージに記載のリンクを参照してください)。

素材がなければ結果を生み出せない

ところで、データベースにシード データが必要な場合も SeedController の出番です。実際の処理はやはり Schema クラスで行いますが、使用するメソッドは SampleEntries です。今回のシステムにはここに配置する必要がある実際のシード データがないため、SampleEntries メソッドはそのままにしておきます。

SeedController をコンパイルすると sidekick によってプロジェクトが再配置されますが、多くのコントローラーと同様に、HTTP 要求が到達しない限り SeedController は有効になりません。SeedController をまだご覧になっていない場合は、ざっと確認してください。SeedController では、PurgeDB、Exports、All、および SampleEntries という 4 つの POST エンドポイントを公開しています。もちろん手動でエンドポイントに到達することもできますが、これは自動化 (Oak の場合は rake) が最適な反復作業です。当然ながら、"rake reset" を実行するとすべてのテーブルが削除され、スキーマが再生成されます (/seed/PurgeDB への POST が実行され、/seed/all への別の POST が実行されます)。"rake sample" を実行するとテーブルが削除され、スキーマが再生成され、サンプル データが生成されます (/seed/SampleEntries への POST が実行されます)。ちなみに付け足すと、"rake export" (/seed/export への POST) を実行すれば、エクスポートに使用する SQL ステートメントが返されます。

(ところで、Curl または Ruby の場合は、コマンドラインのコマンド 1 行で POST を実行できます。Rakefile.rb ファイルには、Net::HTTP::post_from を使用した POST の実行方法の例が含まれていて、Rakefile に埋め込まない場合にはとても簡単に切り取って別の .rb ファイルに貼り付けることができます。つまり、Ruby 学習への手軽な入口として利用できます。これをとがめる人はいません。)

ついに動作する

入力ミスがなかったとすれば、rake reset が完了するとブラウザーが更新され、(ブログ タイトル用の) シンプルなテキスト フィールドと "submit" ボタンを備えた Web ページが表示されます。しかし、新しいブログ タイトルを入力するとエラーが表示されます。Blogs は存在していますが、Blogs に関連付けられた Comments というクラスも存在していると (Index.cshtml ビューの内容に基づいて) 想定されているためです。

ブログ エンジン分野で普及しているシンプルなリレーショナル モデルでは、ブログにはコメントに対して一対多のリレーションシップがあります。このシステムでも同じ方式でモデル化するので、まずは、Comment オブジェクトのデータ型が必要です。このオブジェクトもごくシンプルです。事実、Comments オブジェクトは (Blog オブジェクトのように) 本質的に動的オブジェクトであって特筆すべき要素もないので、モデル クラスの定義さえ不要です。平凡な正真正銘の動的オブジェクト (2013 年 8 月のこのコラム「Gemini ライブラリを使用して動的プログラミングに移行する」(msdn.microsoft.com/magazine/dn342877) をご覧になった方はご存じの Gemini 型) で十分です。

(「私たち、もうカンザスにはいないみたいよ、トト。」という気分になったことがない方は、今こそ立ち止まって思案するときです。型の定義で悩むことのないビジネス オブジェクトに取り組んでいるのですから。)

Comments と Blogs のリレーションシップを示すには、2 つの要素が必要です。まず、Comments には (前回のコラムの Blogs と同様に) リポジトリが必要です。

public class Comments : DynamicRepository
 {
 }

さらに重要なことに、一対多のリレーションシップを直接的 (Blog が Comment オブジェクトのコレクションを保持している、つまりこの場合は Comments オブジェクトをフィールドとして保持しているという意味) かつ間接的 (データベースで Blog オブジェクトと Comment オブジェクトが結び付いていることとその結び付き方が、Oak に認識されるという意味) に把握できるように、Blog クラスを少し変更する必要があります。そのためには、リレーションシップを示す Associates という新しいメソッドを導入します (図 3 参照)。

図 3 Blogs と Comments の一対多リレーションシップの表現

public class Blog : DynamicModel
 {
   Blogs blogs = new Blogs();
   // Define comments
   Comments comments = new Comments();
   public Blog() { }
   public Blog(object dto) : base(dto) { }
   // Add an Associates method to add the Comments() method
   IEnumerable Associates()
   {
     // And define the association
     // For othere examples of associations check out the Oak wiki
     yield return new HasMany(comments);
   }
 }

ご覧のとおり、モデルはブログの抽象概念にかなり密接に一致しているため、先ほど述べた必要な作業とそれほど違っていません。動的手法の使用の魅力と実用性がここに現れています。この 1 回の変更 (Associates メソッドで実行する HasMany の "yield return") で、実際には Blog に追加する 3 つの新しいメソッド (Comments、CommentIds、および NewComment) をトリガーして、Comment オブジェクトと Blog オブジェクトのリレーションシップをサポートしています。これらのメソッドは純粋なスキャフォールディングなので、通常は、標準の動的でない C# を使用している場合のように "ハードな" 方法で記述する必要があります。ただし当然ながら、このような処理がデータベースに対して機能するには、コメント テーブルのデータベース記述で支援する必要があります。このため、SeedController (とその後の rake reset) に戻ります (図 4 参照)。

図 4 コメント テーブルのデータベース記述

public class Schema{
   public IEnumerable> Scripts()
   {
     yield return CreateBlogsTable;
     yield return CreateCommentsTable;
   }
   public string CreateBlogsTable() // Here is the function definition
   {
     return Seed.CreateTable("Blogs",
       Seed.Id(),
       new { Name = "nvarchar(255)" },
       new { Body = "nvarchar(max)" }
     );
   }
   public string CreateCommentsTable() // Here is the function definition
   {
     return Seed.CreateTable("Comments",
       Seed.Id(),
       new { BlogId = "int", ForeignKey = "Blogs(Id)" },
       new { Body = "nvarchar(max)" }
     );
   }
 }

ついでに、ブログ エントリを 2 つ追加して、SampleEntries メソッドの使用方法をお見せしましょう。これは皆さんが思っているよりも簡単な処理です (図 5 参照)。

図 5 Blog エントリの追加

public void SampleEntries(){
   var blog1 = new // Store the ID
   {
     Name = "Hello, Oak Blog",
     Body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
   }.InsertInto("Blogs");
   new { Body = "great job!", BlogId = blog1 }.InsertInto("Comments");
   new
   {
     Name = "Here are cat pictures!",
     Body = "Meowem hisum collar sit amet, addipisces lick."
   }.InsertInto("Blogs");
 }

繰り返しますが、動的オブジェクトや拡張メソッドを使用すると、ほとんど C# には見えなくなります (この場合は、動的オブジェクトを作成する以上に Title と Body の自動生成プロパティで匿名オブジェクトを作成し、InsertInto 拡張メソッドを使用して実際の挿入を行っています)。ちなみに、オブジェクトを blog1 ローカル変数にトラップした唯一の理由は、コメントの追加先ブログ ID 値として使用できるようにするためです。

続いて、rake サンプルでデータベースを更新し、ブラウザーを更新します。

次のステップ: ユーザー入力を検証する

この開発方法はだんだん興味深いものになってきています (まだ興味深くなかったとすればですが)。定義済みのモデル型はなくても、作業モデルとデータベース ストレージがあります。SQL スクリプトは必要ありませんでした (ただし、Schema クラスで繰り返し使用しているアドホック メソッドを簡単に SQL スクリプトに変更できることを指摘しておく方が公平でしょう)。また、このような処理を実行するすべてのコードは、デバッグする必要がある場合や単に確認する場合に備えて、スキャフォールディングされたプロジェクトの Oak フォルダーにソース形式で (まだ) 格納されていることに留意してください。

ユーザー入力が適切かどうか確認するユーザー入力の検証など、まだ説明していない作業がいくつかありますが、次回のテーマとして取っておきましょう。

コーディングを楽しんでください。

Ted Neward は Neward & Associates LLC の社長です。これまでに 100 本を超える記事を執筆している Ted は、さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox、2010 年、英語) もその 1 つです。F# MVP であり、世界中で講演を行っています。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com(英語のみ) です。彼がチームの作業に加わることに興味を持ったり、ブログをご覧になったりする場合は、blogs.tedneward.com(英語) にアクセスしてください。

この記事のレビューに協力してくれた技術スタッフの Amir Rajan (Oak プロジェクトの作者) に心より感謝いたします。
Amir Rajan は、.Net Rocks、Herding Code、および Hanselminutes に出演する開発コミュニティのアクティブ メンバーです。彼の専門は、さまざまな .NET ベースの Web フレームワーク、REST アーキテクチャ、Ruby、JavaScript (フロントエンドと NodeJS)、iOS、および F# です。彼はいつも、オープン ソースへの参加、独立系コンサルティング、およびブログ (amirrajan.net、英語) で業界の発展に努めています。連絡先は ar@amirrajan.net(英語のみ) です。