データベース処理

レコードの集合を永続的に保存させ,いつでも取り出せ,さらに加工が できるようにするシステムをデータベース管理システム(DBMS)という。 実用されているDBMSはデータの保存,取り出し以外の豊富な機能を含む 巨大なシステムであるが,簡単なものはRubyプログラムで容易に実装できる。

テキストファイルでの管理

テキストファイルの解析と書き出しはopenメソッドで行なう(既習範囲)。

csvを読んで加工し,csvに書き出す処理は, csvライブラリ を利用する。ただし遅いので(Ruby1.8) 実用性を求めるなら FasterCSVやcsvscanをインストールして使うことになる。

dbm

Unixシステムの多くはdbmという汎用的なデータベースライブラリを 備えている。簡易データベースはこれで十分作成できる。Rubyでは 添付ライブラリの dbm を介して簡単に利用できる。

dbmによるデータベースは Hash と同様 key と value の対の集合をデータとする。ただし,Hashと 違い,keyとvalueがともに文字列(String)でなければならない。

csvで書かれたデータを読み込み,どんどんdbmに追加する例を示す。 csvのレコードは第1フィールドがキーと見なせるとする(重複するものが ないと保証できる)。

dbm-add.rb

#!/usr/bin/env ruby
# coding: euc-jp
require 'dbm'                   # dbmクラスを利用
datafile = "./database"         # データベース名

# CSVデータを読んでハッシュにいれておく
data = Hash.new
while csv=gets
  # 第1フィールドをキー,残りをごっそり文字列として値にする
  if /([^,]*),(.*)/ =~ csv
    data[$1] = $2
  end
end

# ndbm形式のデータベースを開く
DBM.open(datafile) do |x|
  # 開いた時点で既にデータがあれば x に入った状態でスタートする
  for key, val in data          # data の key, val ペアを取り出して繰り返す
    x[key] = val
  end
end

走らせるとデータベース名に .dir .pag を付加したファイルができる(BSDの場合は *.db)。 市のデータファイル町のデータファイル 村のデータファイルを順次処理した推移を観察しよう。

./dbm-add.rb yama-city.csv
ls -l data*
-rw-r--r--  1 yuuji  wheel  16384 Apr 27 11:35 database.db
strings database.* | less
(中を確認して q で終了)
./dbm-add.rb yama-town.csv
ls -l data*
strings database.db | less
./dbm-add.rb yama-vil.csv

strings コマンドは何をするものか調べよ。

(n)dbm形式のデータファイルは makedbm -u で テキスト部分を抽出できる。

●Solarisの場合
makedbm -u database
●NetBSDの場合
makedbm -u database.db

作成したデータベースを読み込むプログラムも簡単で, DBM.open を使えばよい。

#!/usr/bin/env ruby
# coding: euc-jp
require 'dbm'                   # dbmクラスを利用
datafile = "./database"         # データベースファイル名

# ndbm形式のデータベースを開いて順次レコードを出力
DBM.open(datafile, 0666, DBM::READER) do |x|
  # DBM.open(datafile) だけで開いてもよいが,
  # 読み込みだけのときは第3引数を DBM::READER とする
  for key, val in x             # x の key, val ペアを取り出して繰り返す
    printf("%s -> %s\n", key, val)
  end
end

データ追加,ダンプ,検索の機能を選べるようにした プログラム例を示す。

dbm-ops.rb

#!/usr/bin/env ruby
# coding: euc-jp
require 'dbm'                   # dbmクラスを利用
datafile = "./database"         # データベースファイル名

if ARGV[0] == nil then
  STDERR.puts "#{$0} -a [Data.csv]      Add record"
  STDERR.puts "#{$0} -d                 Dump database"
  STDERR.puts "#{$0} -s                 Search on database"
  exit 1
end

case ARGV.shift
when "-a"                       # データの追加登録
  # CSVデータを読んでハッシュにいれておく
  data = Hash.new
  DBM.open(datafile) do |x|
    # DBM.openしてからデータ入力を行なう処理は好ましくない(後述)
    while csv=gets
      # 第1フィールドをキー,残りをごっそり文字列として値にする
      if /([^,]*),(.*)/ =~ csv
        #data[$1] = $2
        x[$1] = $2
      end
    end
  end
when "-d"                       # 一覧出力
  DBM.open(datafile, 0666, DBM::READER) do |x|
    for key, val in x
      printf("%s -> %s\n", key, val)
    end
  end
when "-s"                       # 検索
  STDERR.print "検索キー: "
  kwd = STDIN.gets.chomp!       # STDIN無しだと -s というファイルから読む
  reg = Regexp.new(kwd, nil, 'e') # 大文字小文字区別,EUCで検索
  DBM.open(datafile, 0666, DBM::READER) do |x|
    for k in x.keys
      if reg =~ k then          # マッチしたレコードのみ出力
        printf("%s -> %s\n", k, x[k])
      end
    end
  end
end

dbm-ops.rb を実行してみよ。

ただし,このプログラムの追加登録部分は 頻繁なデータ更新が必要な場合に効率低下を招く潜在的な問題を含んでいる。

データのキー削除には delete メソッドを用いる。

db.delete(key)

とすると該当するキーと値を削除できる。

PStore

Ruby固有の汎用的なデータ永続化クラスが PStore で,ほぼ全てのオブジェクトがファイルに保存できる。 また,dbmと違いひとつのデータファイルに複数のRubyオブジェクトを格納できる。

たとえば,ハッシュと配列の2つの集合値を保存したい場合, 以下のような手順で格納できる。

require "pstore"
db変数 = PStore.new(保存ファイル)
db変数.transaction do
  # db変数を利用した処理
end

db変数db変数[ルート名] の形で,ルート名 で代表される任意のオブジェクトにアクセスでき, そのオブジェクトは transaction 終了時に保存される。

以下の例は,

をそれぞれ格納し,データファイル ps-data に保存するものである。

ps-add.rb

#!/usr/bin/env ruby
# coding: euc-jp
require 'pstore'
datafile = "ps-data"
# 初期値のハッシュをPStoreファイルに保存
# このプログラムを3回実行したのち ps-list.rb を実行する

db = PStore.new(datafile)
db.transaction do
  m = db["山のデータ"] = db.fetch("山のデータ", Hash.new)
  # これでmが保存可能なハッシュになる
  m["富士山"]	= 3776
  m["月山"]	= 1984
  m["鳥海山"]	= 2236

  e = db["偶数"] = db.fetch("偶数", Array.new)
  # これで e が保存可能な配列になる
  e << 2
  e << 4
  e << 6	# 2,4,6を順次配列に追加する
end

上記のプログラムを3回実行後,以下のプログラムを実行してみる。

ps-list.rb

#!/usr/bin/env ruby
# coding: euc-jp
require 'pstore'
datafile = "ps-data"

# datafileに登録されているものを取り出す。
db = PStore.new(datafile)
db.transaction do
  m = db["山のデータ"] = db.fetch("山のデータ", Hash.new)
  e = db["偶数"] = db.fetch("偶数", Array.new)
  # 既存データがある場合でも同じ代入方法でよい
  puts("山のデータのハッシュ")  # mを順次出力
  for k, v in m
    printf("%s\t-> %4d\n", k, v)
  end
  puts("偶数の配列")            # mを順次出力
  puts e.join(", ")
end
./ps-list.rb
山のデータのハッシュ
月山    -> 1984
富士山  -> 3776
鳥海山  -> 2236
偶数の配列
2, 4, 6, 2, 4, 6, 2, 4, 6

このように,前回の値が残ったまま代入されるので, 配列には値が積み重なってゆく。

「商品」と「単価」のkey, valueペアを持つハッシュを永続的に 持ち,追加で増やせるプログラム例を示す。

pstore-basic.rb

#!/usr/bin/env ruby
# coding: euc-jp
require 'pstore'
datafile = 'pricedb'

def showall(hash)
  for k, v in hash
    printf("%-20s%4d円\n", k, v)
  end
end

db = PStore.new(datafile)
db.transaction do
  price = db["price-list"] = db.fetch("price-list", Hash.new)
  while true
    STDERR.print "商品=単価  の形式で入れて下さい(C-dで終了): "
    break if (line=gets) == nil
    redo if /([^=]*)=([\d ]*)/ !~ line # マッチしなかったら redo
    price[$1] = $2.to_i
  end
  puts "\n全商品リストです"
  showall(price)
end

プログラム中で指定している "price-list" は, ルート名といい,任意のルート名に対応する値を定義し,それを保存できる。 ひとつのpstoreファイルに複数のルートを持たすことができる。


本日の目次

yuuji@e.koeki-u.ac.jp