複製 とテーブル Dolly の場合、第 1 部
Dino Esposito
Microsoft Corporation
May 10, 2001
日本語版最終更新日 2001年7月9日
現実の社会では、複製は討論するにはどの面をとっても、とても扱いにくい話題です。ソフト
ウェアでは、複製が非常に役立つ技法になることもあります。実際には、多くの場合実行中の
オブジェクトのインスタンスを複製してほぼ同一のオブジェクトを生成し、かなり独立した方法で
コード体系を管理する程度のことに思いとどまります。
これは、.NET や Microsoft Foundation Class (MFC)、Active Template Library (ATL)、
ActiveX® Data Objects (ADO) などのその他のフレームワーク固有の特徴ではありません。
同じオブジェクトの既存のインスタンスからオブジェクトを作成できるということは、すべての言語や
プログラミング コンテキストで共通して必要なことです。
現実には、ソフトウェアの複製に関して明確かつ共通に容認される規則はありません。オブ
ジェクトの複製は、実験に左右されやすく、通常影響を調査したり、予測することはできません。
そのため、フレームワークとは無関係にすべてのオブジェクトを複製できるわけではありません。さら
に、複製の動作はオブジェクトごとに微妙に異なる可能性があります。例が必要でしょうか ?
文字列は、MFC クラスの "コンストラクタ" または C ランタイム ライブラリ関数 strdup()
を使って高速かつ容易に複製できます。コピーの入手後は、2 つの独立した要素としてコピーとコ
ピー元の要素の両方を使用します。その際、それらをどのように取得したかは問題になりません。
しかし、たとえば ADO Recordset では、コピーとコピー元ではまったく機能セットが異なる
ことがわかります。
.NET での複製は、ICloneable インターフェイスを使用してサポートされます。一部の
オブジェクトは本質的にこのインターフェイスを実装していますが、その他のオブジェクトでは複製能
力を "浅いコピー" に制限しているものもあります。このようなオブジェクトでは、"深いコピー" は手
動でのみ実現できます。(詳細については、この記事の後半および第 2 部でお話します。)
この 2 部構成の記事で、まず ADO と ADO.NET での複製のサポートに関する概略を説明し
ます。次に、ごく最近のニュースのクローン人間に関する話題の中で有名な Dolly (クローン羊) の場合を説明します。
既にご存知かもしれませんが、ここでの Dolly は Beta 1 のコードで私が最初に複製を試みた
DataTable オブジェクトの名前です。
まず始めに、テーブルの複製に関する ADO の考え方を見てみましょう。
ADO での Recordset の複製
幸いなことに、ADO Recordset はメソッド Clone を使って複製できます。既存の
Recordset を複製する場合 Clone を使用します。つまり、同じ内容で、別の変数名を持
つ 2 つ目の Recordset が必要な場合に使用します。Clone を使用すると、接続オブジェ
クトやコマンド オブジェクトを使って新しい Recordset を初期化、作成するよりも高速であることは間
違いありません。
実際に複製処理で得るものは、"同じデータへのポインタ" です。プログラムから独立した
Recordset オブジェクトを使用して、このポインタを利用します。この結果、快適な側面とあ
まり快適ではない側面が生じます。つまり、Recordset のコピーを取得した後、両方のイン
スタンスを単純に都合のよいときに使用することはできません。多くの特性に注意し、十分注意して
処理する必要があります。
"複製された Recordset" が快適な側面は、作成や使用の際に必要なオーバーヘッドが限定
されることです。複製された Recordset は、複製元の Recordset データへの参照を持つオブジェク
ト変数だけを持っています。さらに、複製された Recordset はブックマークを監視するために個別の
バッファを保持します。つまり、Recordset を個別にナビゲートでき、2 つの Recordset の現在位置
をいつでも必要に応じて異なる位置に設定できます。また、複製元の Recordset および複製され
た Recordset は、操作を終了するときやフィルタ処理するときには独立しているように見えます。
複製のあまり快適ではない側面は、複製されたオブジェクトのレコード ポインタは、複製時に複
製元のオブジェクトが保持していた現在位置とは無関係に、常に先頭レコードに位置付けられるこ
とです。快適でない理由は、このことを常に意識しておく必要があるということだけです。さらに、元の
オブジェクトで適用されていたフィルタ マスクが、複製を作成するときに失われます。ただし、いずれの
場合も、次の 2 つの指示を使用すると、状況を復元できます。
Set rsClone = rsOrig.Clone
rsClone.Move rsOrig.AbsolutePosition
rsClone.Filter = rsOrig.Filter
レコードの位置付けに関する注意点は、使用しているカーソルの種類によって異なり、既定の前
方のみを参照するカーソルでは、レコードセット内の任意の場所に移動しようとすると、エラーが発生
します。
以下の図は、元の Recordset と複製された Recordset の両方が、データを共有するのではなく、
個人用ブックマークを利用できることを示すアーキテクチャを図解しています。
図 1. 最初は同じデータを指す複製元の Recordset と複製された Recordset
1 つの Recordset でブックマークが設定されたレコードを取得するために、レコードを前後に移動
するという余分な負荷を必要としないで、複数の "カレント" レコードを管理する必要がある場合に、
複製を使用することは適切な解決策です。
では、2 つの Recordset の 1 つを編集すると、何が起こるのでしょうか ? 複製元の Recordset
と複製された Recordset は、同期して更新されます。したがって、両方の Recordset が変更を検
出します。図で示されたアーキテクチャを考えると、これは同じバッファのデータを共有していることと同
じことなので驚くことではありません。
ただし、これは既にいくつか深刻な問題を生じる可能性があります。いずれにしても、どんな場合
でも、多少の問題点は存在します。ある Recordset でデータが変更されると、その複製のすべてが
更新に直接関係があるかのように通知を受け取ります。繰り返しになりますが、これは同じバッファの
データを共有しているために生じます。
WillChangeField または FieldChangeComplete など、任意の変更で起動さ
れるイベントは、関連するフィールドに関する情報と共に Recordset をイベント ハンドラに渡します。
しかし、この Recordset は、変更が生じた Recordset への単なる参照です。つまり、複製がイベント
を経由して受け取る Recordset のカレント レコードは、複製自体のカレント レコードと異なる可能性
があります。複製に送られるイベントは、レコード ポインタを自動的に再配置しません。これは、必ずし
も悪いことではありませんが、注意しておく必要があることは間違いありません。
更新で、複製元の Recordset と複製された Recordset を互いに結び付けているリンクは、どちら
かからメソッド Requery を呼び出す場合のみ壊れます。Requery は、最初に
Recordset オブジェクトにデータを設定するために使用したクエリ コマンドを再実行することに
より、Recordset オブジェクトのデータを最新状態に更新します。
Requery は、以前に共有していたデータ セットから接続を解除して、複製に独自のセット
のデータを提供します。その結果、複製元の Recordset と複製された Recordset が異なるデータ セッ
トを指すようになります。また、複製元の Recordset で再クエリするときに、ほかの手段を使用
することもあります。以下の図は、それを図解しています。
図 2。複製で Requery を呼び出して、独自のデータ セットを提供する
Requery はそれを起動した複製からのみ複製元の Recordset への接続を解除することに
注意してください。同じ複製元の Recordset から作成されたすべてのほかの複製は、この操作
によって影響はなく、データを引き続き共有します。
深いコピーと浅いコピー
経験の浅いプログラマは、単にオブジェクトを新しい変数に代入することによって、 オブジェクトが複製
されると考えるかもしれません。次のようなコードは、Recordset を複製することはなく、単に同じオブジェク
トに対して新しいポインタを定義します。
Set rs2 = rs1
このような操作を、通常 "浅いコピー (shallow copy)" として呼びます。"浅いコピー" とは、最上位のインターフェイス
だけを複製し、オブジェクトの構造全体を複製しない部分的なコピーです。"最上位のインターフェイス"
を構成するものは、あらゆる状況で同じになるわけではなく、フレームワークごとに異なる可能性がありま
す。単に現在のインスタンスへのポインタを意味する場合やオブジェクト自体を含むことはありますが、子オ
ブジェクトではありません。
浅いコピーとは異なり、完全に独立した、同等の機能を持つコピーを取得するために、オブジェクトを
単に複製することを "深いコピー (deep copy)" と呼びます。
ADO Recordset について話しているときは、変数を複製すると簡潔な "浅いコピー" を持つことにな
ります。Clone を呼び出すと、やや詳細な "浅いコピー" を持つことになります。これらの場合は、
依然としてオブジェクトがデータ バッファを共有しているので、2 個の完全に独立したオブジェクトを得ること
はないでしょう。実際に、変数 rs2 が元の rs1 ADO Recordset オブジェクトの "深いコピー" を持つことに
なるのは、次のコードを使用した場合だけです。
Set rs2 = rs1.Clone
rs2.Requery
"浅いコピー" および "深いコピー" は、.NET ドキュメントで幅広く使用される用語で、
複製がフレームワーク内でどのように使用されるかを説明しています。
.NET での複製
.NET には、値型と参照型の 2 種類の型があります。値型には、基本型、列挙、および
構造体などがあり、スタックに割り当てられます。参照型には、クラスと配列があり、ヒープに
割り当てられます。基本的に、値型はその内容すべてを保持します。対照的に、参照型は
その内容のすべてまたは一部を保持しているメモリ バッファを指しポインタです。
.NET では、異なる機能と能力を提供する 2 つの方法でオブジェクトを複製できます。
.NET オブジェクトは "浅いコピー" または "深いコピー" のどちらでも作成できます。
"浅いコピー" は、オブジェクトのみのコピーです。オブジェクトが複雑な構造を持ち、子オ
ブジェクトへの参照を含む場合、これらは複製されません。"浅いコピー" では、これらの子オ
ブジェクトのすべてが複製元のオブジェクトを参照します。(複製された ADO Recordset では、
ブックマークおよびフィルタを使ってではなく、データを使って多少何かが行われます。)
"深いコピー" は、オブジェクトの完全な "コピー" です。直接的または間接的に複製元
のオブジェクトが参照するすべてが複製される新しいオブジェクトに含まれます。
.NET フレームワークのルート オブジェクト、すなわち Object オブジェクトは、
MemberwiseClone と呼ばれるメソッドを持ち、現在のオブジェクトの "浅いコピー"
を作成します。
protected object MemberwiseClone();
このメソッドは、"浅いコピー" を実装するための .NET オブジェクトの組み込み機能を
表します。このメソッドは、プロテクト メンバでオーバーライドできません。
標準の "浅いコピー" メカニズムがオブジェクトに対して機能しない場合、まったく新し
いメソッドを提供してそれらを複製またはコピーするか、または ICloneable インター
フェイスを実装します。標準の複製、この場合は上記で説明した "浅いコピー" よりも詳
細な複製を行いたい場合は、ある程度検討が必要になります。
ICloneable インターフェイス
ICloneable は複製可能にしたい場合に、.NET クラスが実装する代表的な
インターフェイスです。カスタム インターフェイスを使用すること、および複製を行うためのメソッ
ドを公開することを避ける手段がないことに注意してください。ただし、ICloneable
は複製オブジェクトの標準インターフェイスなので、複製可能なオブジェクトはこれを使用す
る必要があります。
ICloneable は、Clone と呼ばれる 1 つのメソッドだけを含みます。こ
のメソッド内で行うことは、複製しようとするオブジェクトによって異なります。一般的に、
"深いコピー" 機能を提供する場合、または MemberwiseClone の標準機能より
も詳細にコピーする場合にこれを使用します。
"浅いコピー" または "深いコピー" の両方として、Clone を実装できます。一
貫性のために、提供するものがオブジェクトの現在のインスタンスのコピーであることを確認し
てください。
.NET フレームワークには、既に ICloneable を実装している多くのオブジェクトを
提供します。中でも、Array、HashTable、Queue、String、
さまざまな GDI オブジェクト、XmlNavigator、および XmlNode があります。
ADO.NET オブジェクトの複製
複製可能なオブジェクトのセットには、DBCommand、DBConnection、
DBDataSetCommand、DataTableMapping、および SQLParameter
などの ADO.NET オブジェクト群があります。これらすべてのオブジェクトと、派生クラスは、適切で、
より深い複製またはあまり深くない複製を確実に行う Clone メソッドを持ちます。
DBDataSetCommand クラスの Clone メソッドは、SQLDataSetCommand
クラスおよび ADODataSetCommand クラスを使用して参照されます。これは、クラスの完全な
コピーは行いませんが、接続のような共有リソースにあまり影響を与えずに、できるだけ多くの情
報を複製しようとする新しいオブジェクトを返します。
DBDataSetCommand が埋め込むすべてのオブジェクトが "深いコピー" というわけではあり
ません。TableMappings、MissingSchemaAction、および MissingMappingAction
などの構成オブジェクトが複製されます。SelectDBCommand、InsertDBCommand、DeleteDBCommand、
および UpdateDBCommand などのコマンド オブジェクトでは、同じことが当てはまりま
せん。
より明確に言うと、DBDataSetCommand オブジェクトの Clone メソッドは、そのオブ
ジェクトが含むすべての Command オブジェクトで実際に Clone メソッドを呼び
出します。しかし、スケーラビリティの理由から、DBCommand オブジェクトの複製は完全な "深いコピー" ではありません。実際は、コマンドに対するアクティブな接続はコピーされません
が、ある程度共有はされます。
純粋な複製規則では、このような共有は違反であるように思えますが、複製された DBDataSetCommand
と複製元の DBDataSetCommand を同じ接続で、同じように使用できることは、アプリケーション
にとってはたいへん役立つ結果となることは簡単に理解できます。単純に新しい接続を必要とす
る場合、Command オブジェクトの ActiveConnection プロパティを置き換えます。
テーブル Dolly の登場
すべての ADO.NET クラスが複製の拡張されたサポートを提供するわけではありません。
実際、すべてのクラスが、ICloneable を実装するわけではありません。特に、Beta 1
では、DataTable、DataRow、および DataColumn に特別な複
製機能は見つからなかったでしょう。
DataSet オブジェクトではやや事情が異なります。これは、ICloneable
インターフェイスを実装しませんが、Clone および Copy と呼ばれる 2 つの
メソッドを利用でき、2 つのレベルの複製を提供します。特に Clone は、DataSet
の構造のみ、すなわちテーブル、リレーション、および制約を複製します。代わりに、
Copy は、DataSet の内容の実際の "深いコピー" を作成しているスキーマ
とデータの両方を複製します。
Beta 2 では、DataTable と DataRow は、DataSet のような
メソッドの Clone と Copy のペアを公開する予定です。
典型的な "非接続のアプリケーション"、つまり .NET でより好まれる種類のアプリケーション
の DataTables で作業中に、私はサブテーブルの必要性に気づきました。一見、それは記述
することが特に困難なコードであるようには感じませんでした。より詳しく見ていくと、知らない
DataTable 実験材料の、お粗末なテーブル Dolly で遺伝学的な操作を生じさせる
とても骨の折れる作業であることが明らかになりました。
次回またお会いしましょう!
ダイアログ ボックス : DataSet は何のためにあるの?
私は、今日の ADO.NET ドキュメントの一部は DataSets を利用してデータ ソースから
データをフェッチすることを理解しています。作成するために、レコードのセット全体をループする必
要がある場合、「テーブルはなぜより簡単で高速の DateReader を使わないのか ?」と思います。
複数のページ要求にまたがるレコードを管理する必要がある場合、最初にセッションまた
はアプリケーションにレコードを保存せずに、DataSet オブジェクトを使ってこれを行う方法はありま
すか ?
いつ、どれを使いますか ? DataSet は何ができますか ? また、DataSet は何ができない
のですか ?
DataReader および DataSet は、まったく異なる種類のオブジェクトです。
DataReader は接続されて機能し、前方参照のみ、読み取り専用方式でテーブルを
ループできます。つまり、DataReader は、オブジェクトとして作成された次のようなコード
です。
While Not rs.EOF
' 何かを処理します。
rs.MoveNext
Wend
レコードを追加、削除、または変更する予定がない場合、および前後に移動する必要がない
場合、そのような簡単な作業のためになぜそのような負荷の高いオブジェクトを使用するのでしょう
か ? これが、.NET に DataReader が存在する理由です。そのような作業では DataSet
は適切なオブジェクトではありません。
DataSet は、データや規則を "パッケージ化" するインメモリ のデータのコンテナです。
クライアントとデータ ソース間のある種の中間のサーバー側キャッシュとしてメモリ内に DataSet
を保持できます。DataSet のコンテンツを簡単に XML に直列化でき、それをディスク
ファイル、または接続されたクライアントに HTTP を経由で送信できます。さらに重要なことは、
径路上にある対象のプラットフォームや企業ファイアウォールに関係なくこれを行います。
WinForm アプリケーションや Web サービスでは、DataSet は重要なオブジェクトであり、
中間層とクライアント アプリケーション間で渡されます。そのため、DataSet は要求にまたがって
存在するデータを保存しておくものです。このようにセッションまたはアプリケーションを保存することは、
自然なことです。
1 つの要求内で、データへのアクセス、読み取り、使用のみを行う必要がある場合、
DataSet は最善のツールではない場合があります。それに対して、DataReader は
読み取り専用、前方参照のみのカーソルです。これ以上の機能を必要とする場合に、
DataSet オブジェクトを使用します。しかし、このオブジェクトの最適な用途は、
セッション スコープのデータを格納するために使用することです。
|
Dino Esposito は Wintellect に勤務していて、そこで ADO.NET と ASP.NET のトレーニングとコンサルティングを行っています。彼は VB-2-The-Max の共同設立者で、MSDN Magazine に Cutting Edge コラムを連載しています。
|