UPDATE、DELETE、INSERTのAPI

これまでは参照(SELECT)のためのAPIを考えてきましたが、次に更新(UPDATE)、削除(DELETE)、挿入(INSERT)のAPIを考えたいと思います。

なお、GROUP BYに当たる検索方法を検討していませんが、これらはプログラム上で実現できるうえに実装が大変そうなので、実装の優先度を下げたいと思います。

例で使用するテーブル

これまで同様、次のようなテーブルがあるものとして話を進めます。

members: メンバーを格納したテーブル
member_id family_name first_name age sex group_id
1011 中田 祐輔 22 0 1
1234 山下 沙希 17 1 2
1999 宮川 18 0 2
2112 石田 菜々 21 1 1
2345 山崎 千佳 19 1 1
groups: メンバーの所属するグループを格納したテーブル
group_id group_name
1 大学生
2 高校生
PerlShellKoherent::DBでのテーブルの取得

次のようにして、$membersと$groupsがTableオブジェクトを事前に保持しているものとします。また、更新件数格納用に$countを定義してあるものとします。

my $members = $database->table('members');
my $groups = $database->table('groups');

my $count;

UPDATEのAPI

まず始めに、UPDATE、DELETE、INSERTの中でも記述が最も複雑になると考えられるUPDATEについて考えます。

全件更新
UPDATE members SET age = age + 1;

上記のような更新を行うために、Tableクラスにupdateメソッドを用意しようと思います。対応するコードは下記のようになります。戻り値は実際に更新された件数です。

$count = $members->update(sub{
    my $row = shift;
    $row->{'age'} = $row->{'age'} + 1;
});
# 省略形
$count = $members->update(sub{$_[0]->{'age'}++});
対象を指定した更新

上記のように全件に対して更新を行うことはまれで、通常は対象を指定して更新を行います。PerlShellKoherent::DBでも対象を指定する方法を提供しなければなりません。

対象を指定する方法は、データ参照時と共通である方が使いやすいでしょう。SQLでは、SELECT同様WHERE句によって、

-- 男性のメンバーの年齢を1増加
UPDATE members SET age = age + 1 WHERE sex = 0;

のように更新対象を指定します。PerlShellKoherent::DBでも、参照時同様whereメソッドで更新対象を指定しようとすると、次のようなコードになるでしょう。

$count = $members->where(sub{$_[0]->{'sex'} == 0})
                 ->update(sub{$_[0]->{'age'}++});

この方法は一見うまく機能するように思えますが、ある問題を抱えています。

PerlShellKoherent::DBではView(およびそれを継承したTable)クラスではwhereメソッドによって行の絞られたViewオブジェクトを得ます。Tableオブジェクトから直接updateメソッドを呼び出した場合はともかく、whereメソッドで得たViewオブジェクトから元のテーブルを更新にいくためにはどうすればよいでしょうか。しかも、更新対象となる行は、そのViewによって指定されたものでなければなりません。

これを解決するために、テーブルの各行を一意に識別するための行IDというものを導入しようと思います。すべての行は行IDを持つため、whereメソッドで得たViewオブジェクトを介して取り出した各行は、元々のテーブルのどの行のデータであったのかを追跡できるます。これを用いて、更新対象を指定します。

行はハッシュで表されているので、単純に行IDを追加するだけで実現できます。ハッシュに格納する際のキーは、フィールド名との重複を考えて-RID-のような通常フィールド名に用いないような文字を含んだ文字列にします。

ハッシュ以外にも任意のリファレンスを行として用いることができる実装となったため、行IDは行データに含まれるのではなく、内部的にやりとりされるようになりました。
(2009-11-24)

行IDによる更新

SQLと違い、PerlShellKoherent::DBでは行IDを用いて更新を行えるようにしようと思います。具体的には、行IDを含んだ行データを用いて次のように行います。

$count = $members->update($updated_member);

このような更新方法が可能になれば、あるデータを取得し、変更を加え、DBを更新するという一連の処理を次のようなコードで実現できるようになります。

# member_idが1234のメンバーを検索して取得
my $member = $members->index('member_id')->eq(1234)->iterator->next;

if($member){
    # メンバーが存在する場合

    # 姓を変更(結婚など)
    $member{'family_name'} = '佐藤';

    # 新しいデータでDBを更新
    $members->update($member);
}

複数行を更新したいケースも多いと思うので、引数に配列リファレンスを渡すと各行に対して更新する仕様にしたいと思います。

実際には行IDではなくPKによって更新されるような実装となりました。
(2009-11-24)

結合を用いた更新

RDBMS間で統一はされていませんが、結合を用いて更新を行うSQLも存在します。MySQLPostgreSQLOracleの場合について、その例を挙げます。

-- 結合を用いた更新(MySQLの場合)
-- group_nameが'大学生'であるグループに所属するメンバーを更新
UPDATE members INNER JOIN groups ON members.group_id = groups.group_id
SET age = age + 1 WHERE groups.group_name = '大学生';
-- 結合を用いた更新(PostgreSQLの場合)
-- group_nameが'大学生'であるグループに所属するメンバーを更新
UPDATE members AS m SET age = age + 1 FROM groups AS g
WHERE g.group_id = m.group_id AND g.group_name = '大学生';
-- 結合を用いた更新(Oracleの場合)
UPDATE (
    SELECT age FROM members m, groups g
    WHERE g.group_id = m.group_id AND g.group_name = '大学生'
) SET age = age + 1;

PerlShellKoherent::DBでもこのような結合を用いた更新をサポートしようと思います。これについても、データ参照時との共通化を考えると次のようなコードになるでしょうか。

# インデックスを利用しない例
$count = $members->inner_join($groups,
        sub{$_[0]->{'group_id'} == $_[1]->{'group_id'}}
        )->update(sub{$_[0]->{'age'}++});
# インデックスを利用する例
$count = $members->index_inner_join($groups->index('group_id'),
        sub{$_[1]->eq($_[0]->{'group_id'})}
        )->update(sub{$_[0]->{'age'}++});

いずれも、結合を行うメソッドから得たViewオブジェクトのupdateメソッドを呼び出しています。結合の際には結合後の各行に対して、外部表(駆動表)の行IDを引き継ぐことでこれを実現しようと思います。

結合条件によっては外部表が膨れる(外部表のある行が結果表で複数行になる)場合があります。その場合、同一行IDを持つ行が複数更新対象になってしまいます。更新において外部表が膨れるケースは結合条件の記述ミスではないかと考えられるため、エラーを返すのが親切かもしれません。しかし、同一行IDに対して複数の更新が行われたかをチェックするオーバーヘッドを考えると、1行ごとにチェックを行うのはパフォーマンス上望ましくないと思います。ですので、外部表が膨れるケースでは単純に同一行に複数回の更新を行うように実装しようと思います。

DELETEのAPI

DELETEのAPIはUPDATEとほとんど同じです。UPDATEでは更新内容を引数に取りましたが、DELETEでは不要であるために対象の指定のみを行います。

全件削除
DELETE FROM members;
$count = $members->delete;
対象を指定した削除
DELETE FROM members WHERE sex = 0;
$count = $members->where(sub{$_[0]->{'sex'} == 0})->delete;
行IDによる削除
$count = $members->delete($deleted_member);

上記の$deleted_memberは行IDを含んでいる必要があります。例えば、次のような使い方ができます。

# member_idが1234のメンバーを検索して取得
my $member = $members->index('member_id')->eq(1234)->iterator->next;

if($member){
    # メンバーが存在する場合

    # メンバーを削除
    $members->delete($member);
}
結合を用いた削除
# インデックスを利用しない例
$count = $members->inner_join($groups,
        sub{$_[0]->{'group_id'} == $_[1]->{'group_id'}}
        )->delete;
# インデックスを利用する例
$count = $members->index_inner_join($groups->index('group_id'),
        sub{$_[1]->eq($_[0]->{'group_id'})}
        )->delete;

INSERTのAPI

INSERTは新しいデータを追加するだけなので簡単です。UPDATEやDELETEのように対象を指定する方法を考える必要がありません。

INSERT INTO members (member_id, family_name, first_name, age, sex, group_id)
VALUES (3000, '植田', '志保', 21, 1, 0);

上記を実現するには、Tableクラスにinsertメソッドを用意するのが自然でしょう。

my $new_member = {member_id => 3000, family_name => '植田', first_name => '志保',
    age => 21, sex => 1, group_id => 0};
$count = $members->insert($new_member);
# 省略形
$count = $members->insert({member_id => 3000, family_name => '植田',
        first_name => '志保', age => 21, sex => 1, group_id => 0});

配列リファレンスを用いて複数行のデータを渡した場合には、複数のデータを登録できるようにすると便利でしょう。また、下記のようなSELECTを用いたINSERTに対応するために、引数にViewオブジェクトを受け取った場合は、そのViewオブジェクトからイテレータを受け取り登録するという仕様を考えています。

INSERT INTO members SELECT * FROM other_members;
$count = $members->insert($other_members);

まとめ

SQLのUPDATE・DELETE・INSERTはそれぞれ、Tableクラスのupdate、delete、insertメソッドを用いて行うこととします。

whereメソッドやinner/outer_joinメソッドを用いて、データ参照時と同様に更新・削除の対象を指定する仕様とします。whereメソッドなどで得られたViewオブジェクトを介して元のテーブルを更新するために、行を一意に識別するための行IDを導入しようと思います。