The Backyard - PostgresMetadata Diff
- Added parts are displayed like this.
- Deleted parts are displayed
like this.
!DAOを作ろう
!!既存のテーブルからDAOを生成するためには
既存のテーブルからDAOを生成するには、そのテーブルのスキーマ情報が必要だ。
テーブル名を与えることで、カラム名と型が取れれば、とりあえず値オブジェクトの生成とINSERT,UPDATE,DELETEまでは生成可能となる。
既存テーブルという前提だからCREATE TABLEは不要だろう。
すると残りは、キーを指定したファインダーメソッドに必要な情報ということになる。
これらは、各データベースマネージャが持つスキーマ情報から得ることができる。
ここでは、PostgreSQLを利用してみよう。
!!PostgreSQLのメタデータ
PostgresSQLのメタデータはシステムカタログと呼ばれる一連のテーブルに格納されている。
この中で参照する必要があるのは次の4つのテーブルだ。
:pg_class:テーブル、インデックスなどの情報
:pg_type:データ型の情報
:pg_index:インデックスとテーブルの関連
:pg_attribute:カラム情報
詳細については開発者ガイドを参照すればよい。
!!指定されたテーブルのスキーマを取り出すプログラム
いきなり作ってみる。データ型については処理系固有なので直接文字列を使用して参照することにする。
#!/usr/bin/ruby -Ke
require 'postgres'
class TypeMetadata
def initialize(oid, conn)
r = conn.query("SELECT DISTINCT typname,typlen FROM pg_type WHERE oid=#{oid}")[0]
@name = conv(r[0])
end
attr_reader :name
private
@@convt = { 'int2'=>'smallint', 'int4'=>'integer', 'int8'=>'bigint',
'float4'=>'real', 'float8'=>'double precision',
'bpchar'=>'char'
}
def conv(type)
r = @@convt[type]
return type if r.nil?
r
end
end
class << TypeMetadata
Types = {}
def get(oid, conn)
if Types[oid].nil?
Types[oid] = TypeMetadata.new(oid, conn)
end
return Types[oid]
end
end
class ColumnMetadata
def initialize(name, type, tlen, vlen, notnull, pkey)
@name = name
@type = type
@length = nil
@fixlen = nil
@not_null = (notnull != 'f')
@primarykey = pkey
if tlen.to_i >= 0
@fixlen = tlen.to_i
else
len = vlen.to_i - 4
if len >= 0
if type.name == 'numeric'
d = len & 0xffff
if d > 0
@length = sprintf('%d,%d', len >> 16, d)
else
@length = (len >> 16).to_s
end
else
@length = len.to_s
end
end
end
end
attr_reader :name, :length
def not_null?()
@not_null
end
def primarykey?()
@primarykey
end
def type_name()
@type.name
end
def to_s()
s = sprintf("#{@name} #{@type.name}#{@fixlen || @length.nil? ? '' : '(' + @length + ')'}")
if @not_null
s << ' not null'
end
if @primarykey
s << ' primary key'
end
s
end
end
class TableMetadata
include Enumerable
def initialize(name, conn)
@name = name
@cols = []
oid = conn.query("SELECT DISTINCT oid FROM pg_class WHERE relname = '#{name}'")[0][0]
r = conn.query("SELECT indkey,indisprimary FROM pg_index WHERE indrelid=#{oid}")
key = []
r.each do |x|
p x if $DEBUG
next if x[1] != 't'
key = x[0].split
end
r = conn.query("SELECT attname,atttypid,attlen,attnum,atttypmod,attnotnull,attisdropped " +
"FROM pg_attribute WHERE attrelid=#{oid}")
r.each do |x|
p x if $DEBUG
cn = x[3].to_i
next if cn < 0
next if x[6] != 'f'
@cols << ColumnMetadata.new(x[0], TypeMetadata.get(x[1], conn), x[2], x[4], x[5], key.include?(x[3]))
end
end
def show_schema()
@cols.each do |x|
puts x.to_s
end
end
def each
@cols.each do |x|
yield(x)
end
end
def columns()
@cols
end
end
if __FILE__ == $0
if ARGV.length == 0
puts('usage: ruby pgscm.rb table [more talbe...]')
else
psc = PGconn.connect(nil, nil, nil, nil)
ARGV.each do |tbl|
tbl = TableMetadata.new(tbl, psc)
tbl.show_schema
end
psc.close
end
end
ま、こんなもんかな。
!!!注釈
r = conn.query("SELECT indkey,indisprimary FROM pg_index WHERE indrelid=#{oid}")
key = []
r.each do |x|
p x if $DEBUG
next if x[1] != 't'
key = x[0].split
end
の部分は明らかに臭い。indisprimaryのチェックをWHERE句でやるべきに以下に指定すべきに見える。
それはその通りなのだが、この後、ファインダーメソッドを生成することを考えると、ここでは読み飛ばしている非プライマリーキーによるインデックスも生成対象になるわけだ。したがって、本来は、すべてのキーの組み合わせ情報とインデックス名もクラスとして保持することになる。
!!実際の動作を検証する
runit使えよ。
と思いながらも、こんなテストコードでいいや。目で確認だ。
require 'pgscm'
psc = PGconn.connect(nil, nil, nil, nil)
begin
psc.exec('CREATE TABLE pgs_test (' +
' row1 bigint primary key,' +
' row2 boolean,' +
' row3 bytea not null,' +
' row4 character(8),' +
' row5 date,' +
' row6 double precision,' +
' row7 integer,' +
' row8 numeric(8,4),' +
' row9 decimal(12),' +
' row10 real,' +
' row11 smallint,' +
' row12 text,' +
' row13 time,' +
' row14 timestamp,' +
' row15 varchar(253))')
t = TableMetadata.new('pgs_test', psc)
t.each do |col|
puts col
end
rescue
puts $!.message
end
psc.exec('drop table pgs_test')
psc.close
それでは動かしてみよう。CREATE TABLEが作成できるような情報が取得できていればOKとみなして良いだろう。
$ ruby pgtest.rb
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index 'pgs_test_pkey' for table 'pgs_test'
row1 bigint not null primary key
row2 bool
row3 bytea not null
row4 char(8)
row5 date
row6 double precision
row7 integer
row8 numeric(8,4)
row9 numeric(12)
row10 real
row11 smallint
row12 text
row13 time
row14 timestamp
row15 varchar(253)
とこのように、カラム名、型、長さともに取り出すことができた。
!!使用方法
テーブル名とコネクションオブジェクトからTableMetadataとColumnMetadataのインスタンスが生成できるから、後は必要に応じて処理を行えば良い。
!!既存のテーブルからDAOを生成するためには
既存のテーブルからDAOを生成するには、そのテーブルのスキーマ情報が必要だ。
テーブル名を与えることで、カラム名と型が取れれば、とりあえず値オブジェクトの生成とINSERT,UPDATE,DELETEまでは生成可能となる。
既存テーブルという前提だからCREATE TABLEは不要だろう。
すると残りは、キーを指定したファインダーメソッドに必要な情報ということになる。
これらは、各データベースマネージャが持つスキーマ情報から得ることができる。
ここでは、PostgreSQLを利用してみよう。
!!PostgreSQLのメタデータ
PostgresSQLのメタデータはシステムカタログと呼ばれる一連のテーブルに格納されている。
この中で参照する必要があるのは次の4つのテーブルだ。
:pg_class:テーブル、インデックスなどの情報
:pg_type:データ型の情報
:pg_index:インデックスとテーブルの関連
:pg_attribute:カラム情報
詳細については開発者ガイドを参照すればよい。
!!指定されたテーブルのスキーマを取り出すプログラム
いきなり作ってみる。データ型については処理系固有なので直接文字列を使用して参照することにする。
#!/usr/bin/ruby -Ke
require 'postgres'
class TypeMetadata
def initialize(oid, conn)
r = conn.query("SELECT DISTINCT typname,typlen FROM pg_type WHERE oid=#{oid}")[0]
@name = conv(r[0])
end
attr_reader :name
private
@@convt = { 'int2'=>'smallint', 'int4'=>'integer', 'int8'=>'bigint',
'float4'=>'real', 'float8'=>'double precision',
'bpchar'=>'char'
}
def conv(type)
r = @@convt[type]
return type if r.nil?
r
end
end
class << TypeMetadata
Types = {}
def get(oid, conn)
if Types[oid].nil?
Types[oid] = TypeMetadata.new(oid, conn)
end
return Types[oid]
end
end
class ColumnMetadata
def initialize(name, type, tlen, vlen, notnull, pkey)
@name = name
@type = type
@length = nil
@fixlen = nil
@not_null = (notnull != 'f')
@primarykey = pkey
if tlen.to_i >= 0
@fixlen = tlen.to_i
else
len = vlen.to_i - 4
if len >= 0
if type.name == 'numeric'
d = len & 0xffff
if d > 0
@length = sprintf('%d,%d', len >> 16, d)
else
@length = (len >> 16).to_s
end
else
@length = len.to_s
end
end
end
end
attr_reader :name, :length
def not_null?()
@not_null
end
def primarykey?()
@primarykey
end
def type_name()
@type.name
end
def to_s()
s = sprintf("#{@name} #{@type.name}#{@fixlen || @length.nil? ? '' : '(' + @length + ')'}")
if @not_null
s << ' not null'
end
if @primarykey
s << ' primary key'
end
s
end
end
class TableMetadata
include Enumerable
def initialize(name, conn)
@name = name
@cols = []
oid = conn.query("SELECT DISTINCT oid FROM pg_class WHERE relname = '#{name}'")[0][0]
r = conn.query("SELECT indkey,indisprimary FROM pg_index WHERE indrelid=#{oid}")
key = []
r.each do |x|
p x if $DEBUG
next if x[1] != 't'
key = x[0].split
end
r = conn.query("SELECT attname,atttypid,attlen,attnum,atttypmod,attnotnull,attisdropped " +
"FROM pg_attribute WHERE attrelid=#{oid}")
r.each do |x|
p x if $DEBUG
cn = x[3].to_i
next if cn < 0
next if x[6] != 'f'
@cols << ColumnMetadata.new(x[0], TypeMetadata.get(x[1], conn), x[2], x[4], x[5], key.include?(x[3]))
end
end
def show_schema()
@cols.each do |x|
puts x.to_s
end
end
def each
@cols.each do |x|
yield(x)
end
end
def columns()
@cols
end
end
if __FILE__ == $0
if ARGV.length == 0
puts('usage: ruby pgscm.rb table [more talbe...]')
else
psc = PGconn.connect(nil, nil, nil, nil)
ARGV.each do |tbl|
tbl = TableMetadata.new(tbl, psc)
tbl.show_schema
end
psc.close
end
end
ま、こんなもんかな。
!!!注釈
r = conn.query("SELECT indkey,indisprimary FROM pg_index WHERE indrelid=#{oid}")
key = []
r.each do |x|
p x if $DEBUG
next if x[1] != 't'
key = x[0].split
end
の部分は明らかに臭い。indisprimaryのチェックをWHERE
それはその通りなのだが、この後、ファインダーメソッドを生成することを考えると、ここでは読み飛ばしている非プライマリーキーによるインデックスも生成対象になるわけだ。したがって、本来は、すべてのキーの組み合わせ情報とインデックス名もクラスとして保持することになる。
!!実際の動作を検証する
runit使えよ。
と思いながらも、こんなテストコードでいいや。目で確認だ。
require 'pgscm'
psc = PGconn.connect(nil, nil, nil, nil)
begin
psc.exec('CREATE TABLE pgs_test (' +
' row1 bigint primary key,' +
' row2 boolean,' +
' row3 bytea not null,' +
' row4 character(8),' +
' row5 date,' +
' row6 double precision,' +
' row7 integer,' +
' row8 numeric(8,4),' +
' row9 decimal(12),' +
' row10 real,' +
' row11 smallint,' +
' row12 text,' +
' row13 time,' +
' row14 timestamp,' +
' row15 varchar(253))')
t = TableMetadata.new('pgs_test', psc)
t.each do |col|
puts col
end
rescue
puts $!.message
end
psc.exec('drop table pgs_test')
psc.close
それでは動かしてみよう。CREATE TABLEが作成できるような情報が取得できていればOKとみなして良いだろう。
$ ruby pgtest.rb
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index 'pgs_test_pkey' for table 'pgs_test'
row1 bigint not null primary key
row2 bool
row3 bytea not null
row4 char(8)
row5 date
row6 double precision
row7 integer
row8 numeric(8,4)
row9 numeric(12)
row10 real
row11 smallint
row12 text
row13 time
row14 timestamp
row15 varchar(253)
とこのように、カラム名、型、長さともに取り出すことができた。
!!使用方法
テーブル名とコネクションオブジェクトからTableMetadataとColumnMetadataのインスタンスが生成できるから、後は必要に応じて処理を行えば良い。