連載

.NETで簡単XML

第15回 川俣流XMLプログラミングの定石(1)

株式会社ピーデー 川俣 晶
2004/03/06

 ここで問題を以下のように明確化しよう。ディスクの残り空き容量が足りないなどの理由でXML文書の書き込みが失敗した場合、追加したデータが書き込めないことはあきらめるとしても、既存のデータは維持しなければならない、とする。

 これを実現するために、筆者がよく使う定石を書き足したコードを以下に示す。

Private Const filename As String = "c:\sample.xml"

Private Sub insertData(ByVal number As Integer, ByVal count As Integer)
  Dim doc As XmlDocument = New XmlDocument
  doc.Load(filename)
  For i As Integer = 0 To count - 1
    Dim newElement As XmlElement = doc.CreateElement("data")
    newElement.InnerText = "Number " + (number + i).ToString()
    doc.DocumentElement.InsertBefore(newElement, doc.DocumentElement.FirstChild)
  Next

  Dim tempFullPath As String = Path.ChangeExtension(filename, ".$$$")
  Dim bakFullPath As String = Path.ChangeExtension(filename, ".bak")

  doc.Save(tempFullPath)
  If File.Exists(filename) Then
    File.Delete(bakFullPath)
    File.Move(filename, bakFullPath)
  End If
  File.Move(tempFullPath, filename)
End Sub

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
  insertData(0, 1000)
End Sub
サンプル・プログラム2:空き容量不足の対策を行う定石コードを書き足したプログラム(VB.NET版C#版

 これを同じ条件で実行してみよう。例外で中断することは同じだが、サンプル・プログラム2を実行した後に残った書き換え対象のXMLファイル「c:\sample.xml」の内容は以下のようになっている。

<root>
  <data>FirstData</data>
</root>
サンプル・プログラム2を実行した後のXML文書の内容

 このとおり、元のXMLファイルの内容がすべて残されている。このほかに、途中まで書きかけの一時ファイル(拡張子「.$$$」)が残されている。一時ファイルの内容は、以下のようになっている。

<root>
  <data>Number 999</data>
  <data>Number 998</data>
  <data>Number 997</data>
...中略...
  <data>Number 547</data>
  <data>Number 546</data>
  <data>Number 545</da
サンプル・プログラム2を実行した後に残された一時ファイルの内容

 では追加したコードの内容を見てみよう。ここで追加したコードが意図しているアイデアは簡単である。元のファイルは最後まで手を触れないでおき、別のファイル名でファイルを書き込んで、それが成功してからファイル名を変更するという方法である。XML文書に限らず、ファイルを更新する場合の1つの定石といえる。ここでは、生成したいファイルを拡張子「.$$$」の別ファイルとして作成し、作成に成功してからそれを本来のファイル名に変更している(File.Moveメソッド)。元のファイルがあれば(File.Existsメソッド)、拡張子「.bak」のファイルにファイル名を変更している(File.Moveメソッド)。すでにその名前のファイルがあれば、それは削除している(File.Deleteメソッド)。

 なお、一時ファイルの拡張子と、一時ファイルを作成するディレクトリは、これに限定されるものではなく、必要に応じて変更して使うことができる。ただし、一時ファイルを作成するディレクトリは、対象となるファイルと同じドライブ上にないと、スムーズに移動させることができない。それから、ここでは元のファイルを拡張子.bakとして保存しているが、保存しないという選択もあり得るだろう。ここで保存することを選択しているのは、万一、元のファイルの拡張子を.bakに変更してから、一時ファイルを本来のファイル名に変えるまでの間にプログラムが中断するケースを想定してのことである。この場合、拡張子.bakに変更されたファイルと、書きかけの一時ファイルが残されることになるが、当然のことながら書きかけのファイルにすべてのデータは入っていない。元のファイルが拡張子.bakとして残っていれば、その拡張子を変更してデータを復元することができる。もっとも、そのままプログラムを運用してしまえば、拡張子.bakのファイルは消えてしまうことになるので、万全の安全策とはいいにくいのだが、ないよりはマシだろう。

 これで、最低限のファイルの安全性は確保できたが、万全というわけではない。すでに述べたとおり、元のファイルの拡張子を.bakに変更してから、一時ファイルを本来のファイル名に変える間にプログラムが中断する状況になると、古いデータが拡張子.bakのファイルにしか残らないことになる。これを復旧するには、拡張子を変更する必要がある。このような事態が起こる可能性を極限まで減らすために、2回の拡張子の変更は、可能な限り時間を置かずに連続実行する必要がある。この理由により、望ましくないコーディング例を以下に示す。

 サンプル・プログラムの以下の部分は、機能性にのみ注目すると、XmlDocumentオブジェクト(System.Xml名前空間)のSaveメソッドの実行をもっと遅らせてもよいように思える。

doc.Save(tempFullPath)
If File.Exists(filename) Then
  File.Delete(bakFullPath)
  File.Move(filename, bakFullPath)
End If
File.Move(tempFullPath, filename)
サンプル・プログラム2の一部

 そこで、Saveメソッドの呼び出しを、一時ファイルの名前を本来のファイル名に変更するMoveメソッドの呼び出しの直前に移動させることができる。具体的には、以下のような位置に移動させる。

If File.Exists(filename) Then
  File.Delete(bakFullPath)
  File.Move(filename, bakFullPath)
End If
doc.Save(tempFullPath)
File.Move(tempFullPath, filename)
Saveメソッドの呼び出しを遅らせた例

 さらに、書き込んですぐに拡張子を変える処理は無駄に見えるので、ここを以下のようにシンプルに書き直してみよう。

If File.Exists(filename) Then
  File.Delete(bakFullPath)
  File.Move(filename, bakFullPath)
End If
doc.Save(filename)
さらにシンプルにしたコーディング例

 この書き換えは不適切である。なぜなら、既存のファイルを拡張子.bakに変更してから、新しいファイルが作成されるまでの間隔が空きすぎているためだ。ただ空きすぎているだけでなく、例外によって中断が起こる可能性のある処理が挟まっている。もし、Saveメソッド実行中にファイルに書き込めない状況が発生して例外が発生すると、既存のデータは拡張子.bakのファイルに残るだけとなり、正規のファイル名のファイルは存在しないという状態になってしまう。つまり、次にデータを読み込もうとすると、データがないという状況になる。これは好ましい状況ではない。

 もう1つ、ファイルのロックが適切に行えないことにも注意が必要だ。異なる複数のプロセスから1つのファイルを扱う場合、ファイルを一時的にロックして、ほかのプロセスがアクセスできないようにしてから、内容を書き換えるという方法がある。しかし、このケースでは、元のファイルと新しいファイルは同一のファイルではない。これは以下のような問題を引き起こす。

 つまり、ファイルを読み取ってから保存するという作業が同時に発生した場合、次のような手順で処理が進行する恐れがある。

  1. プロセスAがファイルを読み込む
  2. プロセスBがファイルを読み込む
  3. プロセスAがデータaを追加してファイルを書き込む
  4. プロセスBがデータbを追加してファイルを書き込む

 この結果、ファイルは一度追加されたはずのデータaを含まない状態で残ることになる。これを回避するために、ファイルの書き込みが終わるまで元ファイルをロックしたままにしておくという方法もあるが、最終的にファイル名を入れ替える処理が入るため、ロック対象のファイルが変わってしまい、厳密に常に正しく動作するか分からない。

 筆者の定石ではこのようなパターンへの対処は含まれていない。それは、いまのところ、1つのプロセスだけが、1つのファイルに対してアクセスするような構造でプログラムを作成しているためだ。ファイルのロックについて悩むよりも、このような構造で設計する方がベターだと考えている。より正確にいえば、あるファイルにアクセスする窓口となるオブジェクトは1つしか存在しない、という構造にするようにしている。このような構造にすると、そのオブジェクトが知らないうちにファイルが書き換えられることがなくなり、読み込んだXML文書の内容をずっとメモリ上に保持しておくことができる。これによって、パフォーマンスの向上も期待できるのである(これについては次回で紹介する)。そのようなメリットを得るという前提から逆算して構築された定石である。

 さて、この定石にはもう1つの問題がある。ここで示したような状況でディスクの空き容量が足りない例外が発生すると、そのファイルへのアクセスがロックされたまま残ることである。そのため、書き込みに失敗した一時ファイルを削除するコードなどを入れても、アクセスできずに例外を発生して失敗する。正しい挙動ではないように思うのだが、現在の.NET Framework 1.1で発生している状況である。これに対処する方法は、現在の筆者には手持ちがない。いまのところ、ディスク不足の状況が発生した場合は、そのことをレポートした後で、プロセスを終了してしまうのが妥当な選択だろう。


 INDEX
  .NETで簡単XML
  第15回 川俣流XMLプログラミングの定石(1)
    1.XMLプログラミングにおける定石の必要性
    2.XMLファイル書き込み時の問題点
  3.安全なXMLファイル書き込みのための定石
    4.XML文書の読み書きにどの方法を使うか
    5.シリアライズを使った読み書きの問題点
 
インデックス・ページヘ  「連載 :.NETで簡単XML」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間