Win32OLE 活用法 【第 3 回】 ADODB

書いた人:cuzic

プロローグ

あなたは、膨大にあった Excel 表を解析して、業務を少しずつ自動化して いきました。 自動化していくにつれて、今までの仕事がとても楽になり、満足していました。

けれど、そういう作業を続けているうちにある壁にぶつかりました。

「どうにかならないかな?」

今やっているやり方では、最新の情報に更新しようと思うたび Excel 表を 毎回パースすることになります。 このパースするのにかかる時間が長くどんどん面倒に思えてきたのです。

「昔かじったデータベースの知識を使って、このデータをなんとか できないかなぁ」

はじめに

前回の記事では、Excel の表に入ったデータを Ruby を用いてどのように 取得するかについて学びました。

Excel 表を単純にパースするだけでも、かなり業務を楽にしていく ことができるでしょう。 Excel 表の値を参照するような仕事は面倒なものです。

しかし、ある程度大規模になると Excel は便利とはいえなくなってきます。 例えば、Excel ではデータの入力と表示の形式に区別がないため、 入力には楽な形式、表示には楽な形式というのができません。 また、分析をするときにもいろいろと不便なことが多くなります。

こういう問題に対処するために、データベースを利用する方法があることは みなさんもご存知でしょう。

今回の記事で私たちは、Ruby の Win32OLE ライブラリを使って、データベースを扱う 方法について学びます。

今回の目的

Microsoft が提供している COM コンポーネントの中には、 ADO (ActiveX Data Object) と呼ばれる一連のコンポーネントがあります。

これらのコンポーネントを使えば、データベースに対するデータの 入力や取得ができます。

ADO にはよく使うコンポーネントとして、ADODB.Connection と呼ばれる コンポーネントと ADODB.Recordset と呼ばれるコンポーネントがあります。 ADODB.Connection というオブジェクトは、データベースへの接続を表現する オブジェクトで、Recordset は、レコードセットを表現するオブジェクトです。 レコードセットとは、SQL の SELECT 文を実行した結果のデータの集まりを 言います。

今回の記事では、これらのオブジェクトを使って次の内容の動作を行います。

  • ADO の概要
  • SQL を用いたデータの追加
  • データベースからのデータの取得
  • ADODB.Recordset を用いたデータの更新

まず、ADO ではどのようなオブジェクトが提供されていて、 利用できるのかについて学びます。

次に実用的に CSV 形式のデータをデータベースに入力する スクリプトを例にして、ADO の Connection オブジェクトの 使い方を学びます。

そして、データベースからデータを取得する方法について学びます。

今回の記事では、データベースや SQL が何なのか、ということを すでに知っている人を主な対象としています。 これらについて学びたい方は、別途これらについて解説した 文章を参考にしてください。

ADO の概要

ADO は、データベースにアクセスするための一連の COM コンポーネントです。

ADO を用いることで、データベースに保存されたデータに 直接アクセスして、次の操作ができます。

  • レコードとそのデータの取得
  • レコードの追加
  • レコードの削除
  • レコードの検索

ADO のコンポーネントの中でも今回は特に、Connection オブジェクトと Recordset オブジェクトの 2 つのオブジェクトについて学びます。 また、レコードの値の取得や更新を行うときに用いる Fields コレクション、 Field オブジェクトの使い方についても学んでいきます。

具体的な使い方についてはこれから一緒に学んでいきましょう。

ADO でデータを入力

Excel 表でパースしたデータをまずどうしたいかと言えば、 データベースに入力したいですよね。 データベースに入力することで、検索や更新を簡単に 行いやすくなります。

Excel 表でパースしたデータを CSV 形式にすることは 簡単です。 そこでこの章では、CSV 形式のデータをデータベースに 追加していく方法について学んでいきます。

まず、既存のデータベースに対して接続し、そのデータベースの あるテーブルに対して一行ずつデータを追加するスクリプトを 例に次の項目について学んでいきましょう。

  • 接続文字列とは何か
  • 接続の開き方
  • アクションクエリの実行

次に CSV 形式のデータを取得して、データベースに追加 していくスクリプトを紹介します。

前準備

今回のスクリプトを実行するには、まず操作するための データベースを用意してあげる必要があります。

今回は、Microsoft Office に含まれている Access の mdb 形式のデータベースをサンプルのデータベースとして用います。

このデータベースは次のようなテーブルとフィールドを 持ちます。

designview.png

サンプルとして、次の mdb ファイルを用意します。 ダウンロードして使ってください。

sample1.mdb

一行レコードを追加するスクリプト

insert1record.rb

   1|require 'win32ole'
   2|
   3|conn = WIN32OLE.new("ADODB.Connection")
   4|connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=sample1.mdb"
   5|conn.Open connstr
   6|
   7|begin
   8|  sql = "INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " + 
   9|    "VALUES ('関東大震災','1923/09/01',7.9,142807,TRUE);"
  10|  conn.Execute sql
  11|rescue
  12|  STDERR.puts sql
  13|  STDERR.puts $!
  14|end
  15|
  16|conn.Close

順に説明していきます。

conn = WIN32OLE.new("ADODB.Connection")

ProgID が ADODB.Connection の COM オブジェクトを作成する行です。 conn が Connection オブジェクトになります。 この Connection オブジェクトを用いて、どのデータソースへの接続するかを 指定したり、接続を開いたり、閉じたり、SQL を実行したりします。

connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=sample1.mdb"
conn.Open connstr

Connection オブジェクトの Open メソッドは引数に接続文字列(Connection String) をとります。 接続文字列というのは、引数名=値 のフォーマットの引数列を セミコロンで連結したものです。 この接続文字列の Driver を適切に設定することで Microsoft Access や SQL Server さらには ODBC を経由することで PostgreSQL に対して、 というようにさまざまな DBMS に対して接続できます。

接続文字列について詳しく知りたい方は、接続文字列の作成や、 データ ソースにアクセスする を参考にしてください。

今回は、「Microsoft Access」の mdb ファイルにアクセスします。 そのときは上記のように "DRIVER={Microsoft Access Driver (*.mdb)}" と指定します。 Dbq 以降の引数はデータソースドライバに対して渡される引数となります。 Microsoft Access Driver の場合は、その mdb ファイルがどこにあるのかを指定するために  dbq という名前付引数を使用します。 パスを指定するのに使う引数はデータソースドライバによって異なります。 詳しくは上記のリンクを参照してください。

begin
  sql = "INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " +
    "VALUES ('関東大震災','1923/09/01',7.9,142807,TRUE);"
  conn.Execute sql
rescue
  STDERR.puts sql
  STDERR.puts $!
ensure
  conn.Close
end

繰り返しになりますが、この記事では SQL の構文についてはすでに 知っていることを想定します。 SQL について知りたい方は Microsoft Access のヘルプや、Google で 検索して調べてください。

INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " +
     "VALUES ('関東大震災','1923/09/01',7.9,142807,TRUE);

の SQL ステートメントは、earthquake テーブルに対して、関東大震災のデータを 追加する文になります。 この SQL ステートメントを実際に実行しているのが次の行です。

   conn.Execute sql

この行で conn という Connection オブジェクトが接続しているデータソースに対して SQL ステートメントを実行します。

Connection オブジェクトの Execute メソッドで、アクションクエリを 実行したり、選択クエリを実行してレコードセットを得たりできます。 アクションクエリとは、更新クエリ (UPDATE 〜)、削除クエリ (DELETE 〜)、 追加クエリ (INSERT INTO 〜)、テーブル作成クエリ (SELECT 〜 INTO 〜) の 4種類の SQL ステートメントを指します。 選択クエリは、SELECT ステートメントのようなデータを取得するための SQL ステートメントです。 後で詳しく説明しますが、Connection オブジェクトでは、選択クエリを 実行しても、データの取得はできますが、更新はできません。

この SQL は、アクションクエリの中でも特に「追加クエリ」になります。

begin 〜 rescue 〜 ensure 〜 end は前回も出てきました 始め処理終わり処理のイディオムです。Java なら Before/After パターンと呼ばれたりします。 結城浩さんのデザインパターン紹介 が参考になります。

rescue
  STDERR.puts sql
  STDERR.puts $!

で、エラーが生じたときに実行させた SQL ステートメントを出力しています。 このようにエラーがあったときに SQL を表示すれば、 デバッグの手助けになります。

以上のような簡単なスクリプトで、簡単に SQL ステートメントを実行できます。

CSV からのデータを追加

いよいよ、この節では、CSV からのデータの追加について説明します。

この節で学ぶ項目は次のとおりになります。

  • ARGF を使った引数で示されたファイルを扱う
  • %w() を使った文字列の配列リテラル
  • Enumerable#zip
  • Enumerable#map

それでは、サンプルのスクリプトについて見てみましょう。 このスクリプトは Ruby 1.8 以上で動作します。

csv2rs.rb

   1|require 'win32ole'
   2|
   3|def startADO filename 
   4|  cn = WIN32OLE.new("ADODB.Connection")
   5|  connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=#{filename}"
   6|  cn.Open connstr
   7|  begin 
   8|    yield cn
   9|  ensure
  10|    cn.Close
  11|  end
  12|end
  13|
  14|csvfields = %w(Day Name Magnitude NumOfDeaths DeadOrAlive)
  15|insertfields =  [
  16|  ['Name',false],
  17|  ['Day',false],
  18|  ['Magnitude',true],
  19|  ['NumOfDeaths',true],
  20|  ['DeadOrAlive',true]]
  21|
  22|startADO("sample1.mdb") do |conn|
  23|  ARGF.each_line do |line|
  24|    record = {}
  25|    csvfields.zip(line.chomp.split(/,/)) do |field,value|
  26|      record[field] = value
  27|    end
  28|    values = insertfields.map do |field,rawvalue| 
  29|      if rawvalue
  30|        "#{record[field]}"
  31|      else
  32|        "'#{record[field]}'"
  33|      end
  34|    end
  35|    fieldStatement = insertfields.map{|f,v| f}.join(',')
  36|    
  37|    sql = "INSERT INTO earthquake (#{fieldStatement}) " + 
  38|      "VALUES ( #{values.join(',')} );"
  39|    begin
  40|      conn.Execute sql
  41|    rescue
  42|      STDERR.puts sql
  43|      STDERR.puts $!
  44|    end
  45|  end
  46|end

conn = WIN32OLE.new("ADODB.Connection")
connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=sample1.mdb"

この 2 行については先ほど学んだところです。 conn が Connection オブジェクトで、connstr が 「Microsoft Access Driver」 を使用する接続文字列です。

conn.Open connstr

この行で Connection を開きます。

csvfields = %w(Day Name Magnitude NumOfDeaths DeadOrAlive)

%w() とすることで、要素が文字列の配列を生成できます。

この行は、入力する CSV が持つフィールドとその順番を与えています。 CSV から入力したいときは、どの列がテーブル上のどのフィールドに 対応するかをあらかじめ与える必要があります。 このスクリプトでは、そのためにこの csvfields という変数を用いています。

ARGF.each_line do |line|
  ...
end

ARGF というのは、 スクリプトに指定した引数をファイル名とみなして、それらのファイルを 仮想ファイルとしたオブジェクトです。 簡単なスクリプトを作っているときに、引数のファイルを開く処理を 記述する手間を省けるので、便利です。

  csvfields.zip(line.chomp.split(/,/)) do |field,value|
    record[field] = value
  end

ここで、先ほど用意した csvfields という文字列の配列を 使って CSV の一行ずつを record というハッシュに フィールド名とその値のペアを格納させていきます。

Enumerable#zip は、Ruby 1.8 以上の場合、存在するメソッドです。 csvfields の各要素と、zip の引数の各要素を組み合わせて、 ブロックに渡します。

line.chomp.split(/,/) は、分かると思います。 カンマ区切りの CSV を配列にしています。

zip を使っているところを初めてみた場合は、 Ruby リファレンスマニュアルの Enumerable の説明 をよく読んでみてください。

Enumerable#zip を使うことで複数の配列を最初の要素や、2 番目の要素という ように順にブロックに渡すことができるので便利です。

ここでは、csvfields のフィールド名が各要素となり、 line.chomp.split(/,/) で、対応するフィールドの値の値となります。 それを |field,value| で受けて、record というハッシュに 格納していきます。

zip についての説明はこれくらいにして、これから 次の SQL を生成する部分について学んでいきましょう。

  values = insertfields.map do |field,rawvalue|
    if rawvalue
      "#{record[field]}"
    else
      "'#{record[field]}'"
    end
  end

insertfields は、二つの要素を持つ配列の配列となっています。 その二つの要素とは、フィールド名と そのフィールドの値を シングルクオートで囲まず、生の値として出力する必要があるか どうかを示すフラグの二つです。

Enumerable#map は、前回も出てきましたが覚えて いただけているでしょうか?

ここでは、INSERT INTO 文の VALUES 以下の句を 生成するために使っています。

if rawvalue

という行で、rawvalue という条件式が真かどうかを評価して、 分岐しています。 C などの言語とは異なり、Ruby では制御構造は式です。 つまり、Ruby の if 文は値を返します。 if 文の返す値は最後に評価した式の結果となります。

なお、Ruby では false または nil だけが偽で、それ以外は 0 や空文字列も含めてすべて真です。

そして、Enumerable#map メソッドでは各要素に対してブロック を評価した結果をすべて含む配列を返します。

説明がまわりくどくなってしまいましたが、結局、 values = insertfields.map do |field,rawvalue| ... end という一連の処理では、この if 文を評価した値を各要素と する配列を values に代入することになります。

  fieldStatement = insertfields.map{|f,v| f}.join(',')

また map が出てきましたね。今度は簡単です。 最初の要素の配列を作っているだけです。

  sql = "INSERT INTO earthquake (#{fieldStatement}) " +
    "VALUES ( #{values.join(',')} );"

今まで下準備をしてきましたが、ここで一気に SQL を 組み立てあげます。 こうすることで、フィールドの対応関係などを指折り 数えたりすることなく、SQL を生成できるように なります。

  begin
    conn.Execute sql
  rescue
    STDERR.puts sql
    STDERR.puts $!
  end

ここは先ほどの一行ずつレコードを追加するときのスクリプトと同じ記述です。 conn.Execute で、sql を実行しています。

それではこのスクリプトを実際に実行してみましょう。

サンプルの CSV がここにあります。 sample1.csv

1923/09/01 00:00:00,関東大震災,7.9,142807.0,true
1994/10/04 00:00:00,北海道東方沖地震,8.1,0.0,false
1995/01/17 00:00:00,阪神淡路大震災,7.2,6418.0,true
2004/10/23 00:00:00,新潟県中越地震,6.8,37.0,true

先ほど、関東大震災の行をすでに追加してしまっているときは、 その行を実行するときに除いてください。

ruby csv2rs.rb sample1.csv

と、実行すると csv に含まれるレコードをデータベースに 追加できます。

データソースのレコードの参照

前の章では、データソースに対してデータを追加していく方法について 学びました。

それでは、データソースに現在格納されているデータをどのようにして、 取得できるのでしょうか?

この章では、次の項目について学びます。

  • レコードセットとは
  • レコードセットの開き方
  • カレントレコードとは
  • カレントレコードの特定のフィールドの値の取得

では、具体的なサンプルスクリプトを見ていきましょう。

rs2csv.rb

   1|require 'win32ole'
   2|
   3|def startADO filename 
   4|  cn = WIN32OLE.new("ADODB.Connection")
   5|  connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=#{filename}"
   6|  cn.Open connstr
   7|  begin 
   8|    yield cn
   9|  ensure
  10|    cn.Close
  11|  end
  12|end
  13|
  14|module Recordset
  15|  def [] field
  16|    self.Fields.Item(field).Value
  17|  end
  18|
  19|  def []= field,value
  20|    self.Fields.Item(field).Value = value
  21|  end
  22|  
  23|  def each_record
  24|    if self.EOF or self.BOF
  25|      return 
  26|    end
  27|    self.MoveFirst
  28|    until self.EOF or self.BOF
  29|      yield self
  30|      self.MoveNext
  31|    end
  32|  end
  33|end
  34|
  35|
  36|startADO("sample1.mdb") do |cn|
  37|  sql = "SELECT * FROM earthquake;"
  38|  rs = cn.Execute(sql)
  39|  rs.extend Recordset
  40|  fields = ["Day","Name","Magnitude","NumOfDeaths","DeadOrAlive"]
  41|  rs.each_record do |rs|
  42|    values = fields.map do |field|
  43|      rs[field]
  44|    end
  45|    puts values.join(",")
  46|  end
  47|end

このスクリプトでは、earthquake テーブルの値をカンマ区切りで 出力します。

まず、startADO メソッドによって、データソースへの接続を行います。

def startADO filename
  cn = WIN32OLE.new("ADODB.Connection")
  connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=#{filename}"
  cn.Open connstr
  begin
    yield cn
  ensure
    cn.Close
  end
end

このメソッドは、filename を引数とします。 filename は Microsoft Access のデータベースのファイル名です。 すると、引数付きブロックに、Connection オブジェクトを 渡し、ブロック実行後に Connection オブジェクトを閉じます。

SQL ステートメントを実行した結果となるレコードセットを得るには Recordset オブジェクトの Open メソッドを利用します。

begin 〜 ensure 〜 end の構文は今まで何度もでてきた Before/After パターンのイディオムです。もう慣れてきました でしょうか?

   sql = "SELECT * FROM earthquake;"
   rs = cn.Execute(sql)
   rs.extend Recordset

ここでレコードセットを開きます。 Connection オブジェクトの Execute メソッドを用いて Recordset オブジェクトを作成しています。 詳しくは、ADO 入門講座 の 「レコードセットを開く」のページを参考にしてください。

このページにも書かれているのですが、Recordset オブジェクトを 作成するには、次の 3つの方法があります。

  • Recordset オブジェクトの Open メソッドの利用
  • Connection オブジェクトの Execute メソッドの利用
  • Command オブジェクトの Execute メソッドの利用

Connection オブジェクトの Execute メソッドを利用する例について は先ほど説明しましたね。 前の節で実行していた SQL の INSERT 文をSELECT 文に変更すれば、 Execute メソッドでレコードセットを得ることができます。

しかしながら、Recordset オブジェクトの Open メソッド で得られる Recordset オブジェクトと、Connection オブジェクトや Command オブジェクトの Execute メソッドで得られる Recordset オブジェクトとは違う性質のものになります。 Connection オブジェクトや Command オブジェクトから作成すると 読み取り専用の Recordset オブジェクトが得られます。 これに対して、Recordset オブジェクトの Open メソッドを実行する 場合は、レコードの追加、変更、削除も行える Recordset オブジェクトを 得ることができます。

今回のスクリプトの場合は、参照しか行わないのでどちらの方法でも 可能です。 しかしながら、更新・削除を行う場合は、Recordset オブジェクトを 作成してから Open メソッドでレコードセットを開く必要があります。

    rs.extend Recordset

Object#extend メソッドを使うことで、rs オブジェクトに Recordset モジュールで定義しているレコードセットを操作する ためのメソッドを使えるようにしています。

上記スクリプトの Recordset モジュールでは、レコードセットの中の すべてのレコードのデータを取得するために each_record メソッドを 定義しています。

  def each_record
    if self.EOF || self.BOF
      return
    end
    self.MoveFirst
    until self.EOF || self.BOF
      yield self
      self.MoveNext
    end
  end

このモジュールでは、Recordset オブジェクトのプロパティ、 メソッドを利用して、すべてのレコードを順に操作できるような イテレータとして、each_record メソッドを定義しています。

ここで、レコードセットについて簡単に学んでいきましょう。

レコードセットというのは ADO においてデータソースのデータを 取得、変更するのに使うオブジェクトになります。 このレコードセットは、生成するのに使用した SQL ステートメント に対応するすべてのデータの集まりを持っています。

さらに、レコードセットは常に1つのレコードをカレントレコードと して参照します。 そして、そのカレントレコードの値を、Recordset オブジェクトから 取得、変更などを行うことができます。

ところが、場合によっては SQL に対応するレコードが一行も ない場合があります。 SQL に対応するレコードがあるかどうか調べるのに使うプロパティ が BOF や EOF です。 本来、BOF プロパティはカレントレコードが最初のレコードより前にあるか どうかをしらべるときの プロパティで、EOF はカレントレコードが 最後のレコードより後にあるかどうかを調べるプロパティです。

一致するレコードが一行もない場合は、EOF も BOF も true になります。 一致するレコードがあれば、開いた直後 BOF も EOF も両方とも false になります。

そこで、次のようにして、一致するレコードがなければ、処理をやめています。

    if self.EOF || self.BOF
      return
    end

ここで、self が何なのかを思い出しましょう。

rs.extend Recordset

と実行したため、この変数 rs がこの Recordset モジュールで self が指すオブジェクトとなります。

次に、カレントレコードを移動する方法について学んでいきましょう。

    self.MoveFirst
    until self.EOF || self.BOF
      yield self
      self.MoveNext
    end

ここで、Move 系メソッドについて学びましょう。 Move 系メソッドには次の4つがあります。

  • MoveFirst 先頭のレコードに移動
  • MovePrevious 1つ前のレコードに移動
  • MoveNext 次のレコードに移動
  • MoveLast 最後のレコードに移動

これらのメソッドを組み合わせることで、カレントレコードを 移動させていくことになります。

今回は MoveFirst と MoveNext を利用して先頭のレコードから 順に次の行へと移動していきます。 そして、カレントレコードを移動すると Recordset オブジェクトを yield して、特定の処理を行えるように引数となっている ブロックに処理を渡しています。

この一連の処理を BOF プロパティか、EOF プロパティが false に なるまで繰り返しています。

次は、each_record を実際に実行している部分の動作について学んで いきましょう。

  fields = ["Day","Name","Magnitude","NumOfDeaths","DeadOrAlive"]
  rs.each_record do |rs|
    values = fields.map do |field|
      rs[field]
    end
    puts values.join(",")
  end

まず、fields にテーブルのフィールド名の配列を代入しています。 そして、rs.each_record ですべての行に処理を行わせています。

    values = fields.map do |field|
      rs[field]
    end

では、配列に含まれた各フィールド名に対して、 そのカレントレコードでのそのフィールドの値を得ています。

     rs[field]

という行で、カレントレコードの field という名のフィールドに対応する 値を取得しています。

Recordset モジュールの定義を見ると [] メソッドは次のようになっています。

   def [] field
     self.Fields.Item(field).Value
   end

Recordset オブジェクトの Fields プロパティで得られる Fields コレクションは Field オブジェクトの集まりになります。これは複数形の場合は、それの単数形の オブジェクトのコレクションであるという単純な命名規則にしたがっている 例です。

Fields コレクションの Item プロパティを用いると引数 field に対応する Field オブジェクトが得られます。 Field オブジェクトの Value プロパティが、そのフィールドの値です。

最後に Array#join メソッドで、","区切りで、値を連結して 出力しています。

データソースに対する操作

次のスクリプトの例ではデータソースに対して新しいレコードの追加を 行っています。

addnew.rb

   1|require 'win32ole'
   2|
   3|module Recordset
   4|  def [] field
   5|    self.Fields.Item(field).Value
   6|  end
   7|
   8|  def []= field,value
   9|    self.Fields.Item(field).Value = value
  10|  end
  11|end
  12|
  13|def startRS filename,sql
  14|  cn = WIN32OLE.new("ADODB.Connection")
  15|  rs = WIN32OLE.new("ADODB.Recordset")
  16|  connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=#{filename}"
  17|  cn.ConnectionString = connstr
  18|  cn.Open
  19|  rs.Open sql,cn,3,3
  20|  rs.extend Recordset
  21|  begin 
  22|    yield rs
  23|  ensure
  24|    rs.Close
  25|    cn.Close
  26|  end
  27|end
  28|
  29|sql = "SELECT * FROM earthquake;"
  30|startRS("sample1.mdb",sql) do |rs|
  31|  newrecords = 
  32|    [["Day" , "2000-10-06"],
  33|    ["Name","鳥取県西部地震"],
  34|    ["Magnitude","7.3"],
  35|    ["NumOfDeaths","0"],
  36|    ["DeadOrAlive","False"]]
  37|  rs.AddNew
  38|  newrecords.each do |field,value|
  39|    rs[field] = value
  40|  end
  41|  rs.Update
  42|end

レコードセットの更新・追加においては、まずレコードセットを 更新・追加できるような形で開くことが必要です。

それをしているのが、次の行です。

  rs.Open sql,cn,3,3

第3引数は、レコードセットのカーソルタイプを指定します。 第4引数は、レコードセットのロックタイプを指定します。

カーソルタイプとロックタイプを指定するときは、次の表を 参考にしてください。 この値はオブジェクトブラウザを使うことで参照できます。

カーソルタイプ

定数名説明
0adOpenForwardOnlyレコードセットの先頭から後方へ移動できます。順に参照するときに高速に動作します。 (既定値)
1adOpenKeyset自由に移動できます。参照専用です。
2adOpenDynamic自由に移動できます。他のユーザの更新を参照できます。
3adOpenStatic自由に移動できます。他のユーザの更新を参照できません。

ロックタイプ

定数名説明
1adLockReadOnly読み取り専用です (既定値)
2adLockPessimistic排他ロックを行います
3adLockOptimistic共有ロックを行います
4adLockBatchOptimistic複数のレコードをバッチ更新処理します

カーソルタイプを指定することで、カレントレコードをどのように 移動するかや、他のユーザの更新を考慮するかを決定できます。

ロックタイプを指定することで、データソースへのロックを どういう形で行うのかを決定できます。

第4引数に 3 を与えれば編集可能になり、レコードごとに共有ロックを行います。

前回の記事のように、WIN32OLE::const_load を使うことで COM オブジェクトで定義された定数を使うこともできます。

Recordset オブジェクトを用いてデータソースに格納されたデータを更新したい 場合は、このように rs オブジェクトを作成したのちに、rs.Open メソッドで カーソルタイプやロックタイプを指定して、レコードセットを開きます。

そして、実際に更新するときは次のメソッドを使います。

  • AddNew メソッド
  • Update メソッド

AddNew メソッドはレコードを新規追加するときに 用いるメソッドです。

AddNew メソッドを使うことで、カレントレコードを 新規レコードにできます。 つまり、AddNew メソッドを呼んでから、フィールドの 値を更新することで、新規追加するレコードの 値を設定できます。

SQL の INSERT INTO 文でも新規レコードの追加ができますが、 AddNew メソッドでも新規レコードの追加を行えます。

既存のレコードを更新するには、更新したいレコードに Move 系のメソッドなどで移動して、そしてフィールドの 値を更新することで行えます。

それでは、INSERT INTO 文を用いる新規レコードの追加と Recordset オブジェクトを用いた方法と二つありますが、 どのように使い分けると良いでしょうか? この使い分けは単なる好みで決めても特に問題がないのですが、 Field オブジェクトのプロパティをみて、条件分岐をしたい 場合は、Recordset オブジェクトと AddNew メソッドを使用 することになります。

詳しくは、最後のよく使うメソッド一覧の章を参照して欲しいのですが、 Field オブジェクトにはフィールド名やフィールドの型を取得する プロパティがあります。

フィールドの型や名をプログラム中で参照したい場合は、 AddNew メソッドを使う方法の方が楽にプログラムを書けます。 なお、実行速度については自分が扱うデータベースで実際に 処理させてみてどちらが速いか時間を測ってみた方がいいでしょう。

   def []= field,value
     self.Fields.Item(field).Value = value
   end

Recordset モジュールのこのメソッド定義がデータの更新を行うときに 重要になります。

前回の Excel シートの使い方を説明するときでも、[]= メソッドの 定義を行いましたね。 今回もそれと同様です。 フィールド名と、その更新する値の二つを引数としてとります。

そして、field という名のフィールドの値 (Value プロパティ) を value に更新します。

そして、ここで定義されたモジュールとメソッドを次のように用いて、 新規レコードの追加を行います。

  rs.AddNew
  newrecords =
    [["Day" , "2000-10-06"],
    ["Name","鳥取県西部地震"],
    ["Magnitude","7.3"],
    ["NumOfDeaths","0"],
    ["DeadOrAlive","False"]]
  newrecords.each do |field,value|
    rs[field] = value
  end
  rs.Update

newrecords には、フィールド名とそのフィールドの値が 代入されています。

そして、この配列を使って新規レコードの field という名のフィールドの値を value に更新しています。

  rs.Update

Update メソッドは、更新した内容で確定するときに 必要となります。

実は Update メソッドを明示的に呼ばなくても 先ほどの Move 系のメソッドは、編集中に別のレコードに 移動すると、暗黙的に Update メソッドを呼び、 更新が保存されます。 そのため Move系メソッドと組み合わせて利用するときは、 Update メソッドを特に記述しなくても 更新された値が保存されます。

しかしながら、Update は明示的に呼ぶような習慣を持ちましょう。 そうすることで、そこで一区切りということが分かり スクリプトが分かりやすくなります。 それに今回のスクリプトのように Move 系メソッドを呼ばない場合には、 Update がないとエラーが発生することになります。 Update は書く習慣がある方がそのようなエラーに悩まされずに 済みます。

SQL ステートメントの生成

この章では、ADO と Win32OLE の組み合わせでも もちろん適用できますが、一般的にデータベースを 扱うときに役に立つ事柄について学んでいきます。

SQL ステートメントを生成することはデータベースを 扱う上では必ず遭遇します。

しかしながら SQL ステートメントを生成するときの コードにクールと感じさせられることは 多くありません。 単純な文字列の連結を連ねて書いて SQL ステートメントの生成を 行っているスクリプトをしばしば見かけます。

たとえば、次のようにして SQl を生成する方法があります。

sql = "INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " +
    "VALUES ('#{name}','#{day}',#{magnidude},#{death},#{dead});"

このような方法はパッとみて理解できるという点で優れています。

しかしながら、フィールドの数が多くなるにつれて この方法は破綻してしまいます。 フィールドの数があまりに多いと、長くなってしまい、対応関係が 見てもよく分からず、1 つ抜けてバグになってしまったときに 何が抜けているのかを調べるのに時間がかかってしまったりします。

もっと、簡単でいいやり方はないんでしょうか?

これまでにも何度か説明してきましたが、Ruby には Enumerable モジュールに、いくつか便利なメソッドが 定義されています。 map や join です。

これらのメソッドを組み合わせながら、SQL ステートメントを 生成するようなテクニックについてこの章では学んでいきます。

INSERT ステートメントの生成

INSERT ステートメントを生成するためのテクニックについては CSV からのデータの追加のところで、説明しました。 知りたい方についてはそちらを参照してください。

UPDATE ステートメントの生成

UPDATE ステートメントもEnumerable#map や Enumerable#join を使って SQL を生成することができます。

update.rb

   1|def generateUPDATE tablename,values,constraints
   2|  fields = values.map{|field,value,flag| field}
   3|  field_statement = fields.join(",")
   4|
   5|  sql = "UPDATE #{tablename} SET "
   6|  vs = values.map do |field,value,rawvalue|
   7|    if rawvalue
   8|      "#{field} = #{value}"
   9|    else
  10|      "#{field} = '#{value}'"
  11|    end
  12|  end
  13|  set_statement = vs.join(" , ")
  14|  sql.concat set_statement
  15|  
  16|  where_statement = constraints.map do |field,value,rawvalue|
  17|    if rawvalue
  18|      "#{field} = #{value}"
  19|    else
  20|      "#{field} = '#{value}'"
  21|    end
  22|  end.join(" AND ")
  23|  
  24|  sql.concat " WHERE #{where_statement};"
  25|  return sql
  26|end
  27|
  28|values = [['Magnitude',7.9,true],
  29|  ['NumOfDeaths',142807,true],
  30|  ['DeadOrAlive',"TRUE",true]]
  31|
  32|constraints = [['Name','関東大震災'],['Day','1923/09/01']]
  33|
  34|puts generateUPDATE("earthquake",values,constraints)

generateUPDATE メソッドは、3 つの引数をとります。

  • テーブル名
  • 更新するフィールド名とその値
  • WHERE 句で指定する条件となるフィールドとその値

テーブル名と更新するフィールド名とその値というのは、 INSERT ステートメントのときと同じになります。

違いは WHERE 句で指定する条件となるフィールドと その値を引数とする点です。

これは、第2引数と同じ形式の配列です。

この WHERE 句用に指定した配列を利用して、WHERE 句を生成していきます。

このメソッドでも先ほどと同様に WHERE 句を利用します。

SET の次に続く文字列は次のように生成します。

  vs = values.map do |field,value,rawvalue|
    if rawvalue
      "#{field} = #{value}"
    else
      "#{field} = '#{value}'"
    end
  end
  set_statement = vs.join(",")

map で使っているブロック引数の代入は多重代入と 同じ規則に従います。 この場合 values の要素が配列のとき、その配列の数が 代入される側の個数、この場合は 2個を超えている場合は そのあまった要素は無視されます。 足りない場合、この場合は 1個しかない場合は、 対応する要素のないブロック引数には nil が代入されます。

この記述は慣れると非常に直感的です。 配列 values の 1 要素が更新したいフィールド 1 つに対応していました。 values から map メソッドを利用して、「フィールド名 = 値」の配列 vs を得ます。 そして配列 vs を join メソッドを利用して、連結することで SET 句を生成します。

同様のことを WHERE 句に対しても行っています。 WHERE 句のときの違いは join メソッドで連結するときにコロン(,) ではなく " AND " で連結しているという点です。 そして、次のような表記も目新しいところでしょうか?

  end.join(" AND ")

values.map 〜 end で配列が返されるので、そのまま join を呼ぶことで その配列の各要素を " AND " で連結できます。 一旦変数に格納する手間を省けて便利です。

SELECT ステートメントの生成

同様に SELECT 文を生成するスクリプトについても紹介しておきましょう。

select.rb

   1|def generateSELECT tablename,fields,constraints
   2|  field_statement = fields.join(",")
   3|
   4|  sql = "SELECT #{field_statement} FROM #{tablename} "
   5|  
   6|  where_statement = constraints.map do |field,value,rawvalue|
   7|    if rawvalue
   8|      "#{field} = #{value}"
   9|    else
  10|      "#{field} = '#{value}'"
  11|    end
  12|  end.join(" AND ")
  13|  
  14|  sql.concat " WHERE #{where_statement};"
  15|  return sql
  16|end
  17|
  18|fields = %w(Name Magnitude NumOfDeaths DeadOrAlive)
  19|constraints = [['Name','関東大震災'],['Day','1923/09/01']]
  20|
  21|puts generateSELECT("earthquake",fields,constraints)

これはもう分かりますね。 しつこく同じ内容を解説することはしません。

よく使うオブジェクトとメソッド

ここまで、ADO を使ってレコードの参照や更新を行う方法について 学んできました。 ADO のオブジェクトの中でも特によく使うオブジェクトと そのメソッドについて簡単にこの章で説明します。

どのような引数をとるかや、実際の使い方については、 MSDN や Google で検索した内容を参照してください。

ADO 関係の COM オブジェクトでよく使うのは次のものです。

オブジェクト説明
Connectionデータソースへの接続です。
Commandデータソースに対して実行するコマンドを保持します
RecordsetテーブルやSQLステートメントを実行して返されたレコードセット
FieldRecordsetオブジェクトの FIeld オブジェクトを格納します

Connection オブジェクト

BeginTrans新規トランザクションを開始します。
Close開いている接続とこの接続に関連する Recordset オブジェクトをすべて閉じます。Recordset オブジェクトの保留中の変更はすべてロールバックされます。
CommitTransすべての変更を保存して現在のトランザクションを終了します。
ConnectionStringデータソースへの接続のために必要な情報を設定するときに使用します。
Executeクエリや SQL ステートメントを実行します。
Openデータソースへの物理的な接続を確立します。
RollbackTransすべての変更をキャンセルして現在のトランザクションを終了します。
Stateオブジェクトが開いているか閉じているかを示します。

Command オブジェクト

CommandText通常は、実行したい SQL ステートメントを設定するときに使用します。
Executeクエリや SQL ステートメントを実行します。
Stateオブジェクトが開いているか閉じているかを示します。

Recordset オブジェクト

AddNew新規レコードの作成と初期化を行います
BOFカレントレコードの位置が最初のレコードより前にあることを示します
Cancel非同期メソッドの中で保留中のものをキャンセルします。
CloneRecordset オブジェクトの複製を作成します。複数のカレントレコードを保持したいときに使います。
Deleteカレントレコードを削除するときに使います。
EditModeカレントレコードに保留中の変更があるかどうかを調べるときに使います。
EOFカレントレコードの位置が最後のレコードより後にあることを示します。
FieldsRecordset が保持する Field のコレクション
Find指定した条件を満たす行を検索します。
GetRows複数のレコードを配列に取り込みます。
GetStringRecordset を文字列として返します。
Moveカレントレコードの位置を移動します。
MoveFirst最初のレコードに移動してそのレコードをカレントレコードにします。
MoveLast最後のレコードに移動してそのレコードをカレントレコードにします。
MoveNext次のレコードに移動してそのレコードをカレントレコードにします。
MovePrevious前のレコードに移動してそのレコードをカレントレコードにします。
OpenRecordsetを開きます
RecordCountRecordsetオブジェクト内のレコード数です。
RequeryRecordset を開くときのコマンドを再実行して、データソースからもう一度取得しなおします。
Resync再同期を行います
SaveRecordset をファイルまたは Stream オブジェクトに保存します。
SeekRecordset のインデックスを検索して、指定値と一致する行にすばやくカレントレコードを移動します。
Supportsレコードの追加、変更などが可能なレコードセットかを調べるときに使います。

Field オブジェクト

Nameフィールドの名前を取得します。
Typeフィールドの型が何かを取得できます。
OriginalValue変更が行われる前のこのフィールドの値です。
Valueこのフィールドの値です。
UnderlyingValueデータベース内のこのフィールドの値です。

おわりに

今回は Win32OLE を利用して ADO を扱う方法について学びました。 ADO については紹介しきれない内容がまだまだあります。 例えば、テーブル構造の変更などを行うには、ADOX という COM コンポーネントを必要とします。

しかしながら、今回紹介した内容が分かれば普通に データベースを扱うには十分でしょう。

今の多くのシステムはデータベースと連携して動作します。 ADO を扱う今回紹介したようなやり方を知っていれば、さまざまな データベースを扱うときに役に立つでしょう。

次回は、私の記事は一旦お休みして、しむらさんによる 記事を掲載していただきます。

cuzic の記事としては次々回に Outlook でメールを読む、 メールを送るといった内容を扱います。

どうぞ、お楽しみ。

(アドバイザー:arton、助田 雅紀)

著者について

cuzic は、親指シフトキーボードを自在に操る Ruby プログラマです。

風邪を引いて寝込んだときは、フルーツを食べてあったかいものを 飲んで過ごします。 医者には行きません。

更新日時:2005/09/20 13:49:32
キーワード:
参照:[Rubyist Magazine 0005 号] [0005 号 巻頭言] [各号目次] [prep-0005]