簡易データベース処理

レコードの集合を永続的に保存させ,いつでも取り出せ,さらに加工が できるようにするシステムをデータベース管理システム(DBMS)という。 実用されているDBMSは単純なデータの保存・,取り出しだけでなく 検索や資源管理などを豊富な機能を含むシステムである。

DBMSの利用は作成するプログラムで扱えるデータの規模を飛躍的に 増大させられるという利点もあるが,利用するためにはDBMS上での データ構造の構築方法について一定量学習する必要がある。 最終的な目標はDBMS下で管理するデータの利用を置きつつ, ここでは Ruby プログラムから容易に利用できるシンプルなデータベース機構を取り扱う。 データの取り出しだけでなく,追加や更新機能を付けたい場合は これらのものが有用となる。

dbm

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

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

DBM.open(dbmファイル) do |変数|
  ...
end

の形式で利用し,「変数」と「dbmファイル」 を結び付ける。この変数にはハッシュのように利用でき,do ... end ブロックを抜けると自動的に変数の値がdbmファイルに保存される。

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

dbm-add.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
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
●FreeBSDの場合
yp_mkdb -u database.db

LinuxではNISサーバパッケージをインストールすると makedbm コマンドが使用できるものが多い。 もし makedbm コマンドが見付からない場合は,以下のスクリプトで代用できる。

dumpdbm.rb

#!/usr/bin/env ruby
require 'dbm'
DBM.open(ARGV[0]){|d| d.each{|i| printf("%s %s\n", *i)}}

dumpdbm.rb ではデータベースファイルの拡張子を省略して起動する。

./dumpdbm.rb database

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

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
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: utf-8 -*-
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データを読んでハッシュに入れておく
  DBM.open(datafile) do |x|
    # DBM.openしてからデータ入力を行なう処理は好ましくない(後述)
    while csv=gets
      # 第1フィールドをキー,残りをごっそり文字列として値にする
      if /([^,]*),(.*)/ =~ csv
        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, "n")	# "n" 文字コード変換なしで検索
  DBM.open(datafile, 0666, DBM::READER) do |x|
    require 'kconv'
    for k, v in x
      if reg =~ k then          # マッチしたレコードのみ出力
        printf("%s -> %s\n", k, v)
      end
    end
  end
end

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

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

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

db.delete(key)

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

ユーザ名とパスワードなど,キーと値の対で管理する類のデータは dbm が向いている。dbmは,それがインストールされているシステム上では 同一のファイル形式であることが保証される。このため,別の言語やツールと データを共有する必要がある場合にはdbm形式を用いるのが有利である。 ただし,同じ種類のdbmをインストールしている場合でも, 計算機の種類が違うとデータに互換性がない場合があることに注意する。

PStore

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

PStoreを使う場合の基本的な流れは以下のとおりである。

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

db変数 はほぼハッシュと同様に利用でき, 添字にキーを指定するとそれに対応する値を取得できる。 PStoreではキーのことを「ルート名」と表現し, db変数[ルート名] の形で,ルート名 に対応する任意のオブジェクトにアクセスでき, そのオブジェクトは transaction 終了時にファイルに保存される。 したがって,db変数 に代入された値はその後の起動でも 利用できることになる。

ここでたとえば,ハッシュと配列の2つの集合値を保存したい場合を考える。 以下の例は,

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

ps-add.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
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: utf-8 -*-
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: utf-8 -*-
require './kprintf.rb'
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

PStoreはプログラム終了時の変数の値をまるごと持ち越せるため, 細かいことをとくに意識しなくてもデータを永続させることができる。ただし, 現状ではRuby固有のものであるため,Ruby 以外の処理系とのデータのやりとりはできない。また,Ruby の内部構造に依存したデータをファイルに書き込むため, データファイルを保存したときのRubyのバージョンと違うバージョンの Ruby では読み取れないこともある。必要に応じて,CSV形式など 他のテキスト形式のファイルに書き出すプログラムなどを 作成しておく必要性なども考慮した方がよい。

なお,PStoreなどの機構を利用して,プログラム動作中の変数の値を, 論理的な構造を保ったままファイルのようなバイト列に変換することを シリアライズ という。

YAML

PStoreとほぼ同様の使い勝手・機能で利用でき,保存データファイルの 汎用性の高いものがYAML (http://yaml.org)である。YAMLは様々な言語で 構造化されたデータをシリアライズするために策定されている仕様で, Rubyからも利用できる。ただし,言語の垣根を越えて利用することが 主眼であるため,保存できるデータの種別が以下のものに限られている。

マッピング- ハッシュ/辞書 (RubyではHashに対応)
シーケンス- 配列/リスト (RubyではArrayに対応)
スカラ- 文字列や数値等

スカラとして保存できるのは,整数,浮動小数点数,文字列,日付,真偽値で, このうち文字列はutf-8コードのもののみ扱える。

値のファイル保存にYAMLを用いるには yaml/store ライブラリを用い以下のような流れで行なう。

require "yaml/store"
db変数 = YAML::Store.new(保存ファイル)
db変数.transaction do
  # db変数を利用した処理
end

PStoreの使用例と比べて分かるように, 変数と保存ファイルを結び付けた後の使用方法は全く同じである。 PStoreの例で示した と同じ機能をYAMLを用いて記述すると以下のようになる。

yaml-add.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'yaml/store'
datafile = "data.yaml"		# データを保存するファイル名
# ハッシュをYAML形式ファイルに保存

db = YAML::Store.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

色を変えて示した部分が PStore からの変更点で,プログラムの根幹は全く変更なしで動くことが 見て取れる。PStoreでの保存ファイルは人間が直接読み書きできない バイナリ形式だが,YAML形式の保存ファイルは可読性が高い。 上記の yaml-add.rb を動かして生成されたファイルを見てみよう。

./yaml-add.rb
(ここでYAMLデータファイルを確認)
cat price.yaml
---
"山のデータ":
  "富士山": 3776
  "月山": 1984
  "鳥海山": 2236
"偶数":
- 2
- 4
- 6

YAML形式では,マッピング(Hash)はキーと値をコロンで区切ったもの, シーケンス(Array)はハイフンで始まる行の並びで表現される。 この書式を守ってファイルを直接編集することで, データの追加や削除を手動で容易に行なうことができる。

なお,YAMLでデータの並びを行ごとに記述する方式を ブロックスタイルという。改行によらず

{キー1: 1, キー2: 2}
[要素1, 要素2, 要素3]

のようにそれぞれマッピング,シーケンスを表現する方式を フロースタイルという。

データのファイル保存をYAMLで行なうのは効率の面ではあまり有利でないが, 保存されたデータを直接確認してプログラム修正の参考としたり, Ruby以外で作成された他のツールに, 構造を保ったままの複雑なデータを渡したりできるなどの利点がある。


本日の目次

yuuji@e.koeki-u.ac.jp