クイックスタート

はじめに

この章は、Tengを使ったことない人が試しに使ってみるガイドを目指しています。

なるべくわかりやすく、そのまま実行可能なように記述してあるつもりですが、多少初心者には不親切かもしれません。

この章では、ある程度、他のORMやDBIの知識のある人が試しにTengを使い始めることを目的としているので、よくあるメソッド等でもあえて紹介する順序を後にしていることがあります。

環境の準備

まず、perlを導入しましょう。perl5.8以降であれば、基本的には動作するはずです。

Tengを以下のコマンドでインストールします。

$ cpanm Teng

この文書では、導入と扱いの手軽さからSQLiteをRDBMSとして用いるので、用意してください。 SQLiteのバージョンは、3.0以降です。

perlからSQLiteを扱うためにDBD::SQLiteを導入します。

$ cpanm DBD::SQLite

DBの作成

まず、SQLiteのデータベースを作ります。コマンドラインで以下のように入力してください。

$ sqlite3 quickstart.sqlite
sqlite> .exit

テーブルの作成

ここでは、ユーザーを管理するプログラムについて考えます。

nameというカラムとageというカラムを持つuserテーブルを作りましょう。

以下のようなプログラムを作ります。

use strict;
use warnings;
use utf8;
use DBI;
use Teng;
use Teng::Schema::Loader;

my $dbh = DBI->connect('dbi:SQLite:quickstart.sqlite', '', '', {
    RaiseError => 1,
    PrintError => 0,
    AutoCommit => 1,
    sqlite_unicode => 1,
});

my $teng = Teng::Schema::Loader->load(
    dbh => $dbh,
    namespace => 'MyApp::DB',
);

$teng->do(q{
    CREATE TABLE user (
        id INT UNSIGNED NOT NULL PRIMARY KEY,
        name VARCHAR NOT NULL,
        age INT UNSIGNED NOT NULL
    )
});

Teng::Schema::Loaderは、dbhから現在のテーブル情報を動的に取得して、Teng用のSchemaを構築してくれるモジュールです。

Teng#doはDBI#doを実行し、エラーが起きた際には、エラーをフォーマットしつつdieしてくれます。SQLの結果を取得せずに任意のSQLを実行したい場合に便利です。

create-table.plという名前で保存し、実行してみましょう。

$ perl create-table.pl

テーブルが実際にできたか確認してみましょう。

$ sqlite3 quickstart.sqlite
sqlite> .schema
CREATE TABLE user (
        id INT UNSIGNED NOT NULL,
        name VARCHAR NOT NULL,
        age INT UNSIGNED NOT NULL
    );
sqlite>
sqlite> .exit

userテーブルができていますね。

試しにもう一度実行してみましょう。

$ perl 001-create-table.pl
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ Teng 's Exception @@@@@
Reason  : DBD::SQLite::db do failed: table user already exists at /Users/yoshimi/perl5/perlbrew/perls/perl-5.12.1/lib/site_perl/5.12.1/Teng.pm line 297.

SQL     :
              CREATE TABLE user (
                  id INT UNSIGNED NOT NULL,
                  name VARCHAR NOT NULL,
                  age INT UNSIGNED NOT NULL
              )

BIND    : $VAR1 = '';

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
 at 001-create-table.pl line 23

これは、既にquickstart.sqliteにuserテーブルが存在しているのに作ろうとしたため、です。

データの挿入

適当なデータを挿入してみましょう。

use strict;
use warnings;
use utf8;
use DBI;
use Teng;
use Teng::Schema::Loader;
use Data::Dumper;

my $dbh = DBI->connect('dbi:SQLite:quickstart.sqlite', '', '', {
    RaiseError => 1,
    PrintError => 0,
    AutoCommit => 1,
    sqlite_unicode => 1,
});

my $teng = Teng::Schema::Loader->load(
    dbh => $dbh,
    namespace => 'MyApp::DB',
);

my $row = $teng->insert(user => {
    id => 1,
    name => 'walf443',
    age  => 26,
});

warn Dumper($row->get_columns);

my $last_insert_id = $teng->fast_insert(user => {
    id => 2,
    name => 'walf444',
    age  => 30,
});

warn $last_insert_id;

ところで、このコードはTengクラスのインスタンスを作るところまでは、前回のコードと同様で冗長ですね。ここまでのコードはこれからも何度か出てきますし、説明上も見辛いので、インスタンスを作る部分までの部分は別ファイルへ外出ししてしまいましょう。

use strict;
use warnings;
use utf8;
use DBI;
use Teng;
use Teng::Schema::Loader;
use Data::Dumper;

my $dbh = DBI->connect('dbi:SQLite:quickstart.sqlite', '', '', {
    RaiseError => 1,
    PrintError => 0,
    AutoCommit => 1,
    sqlite_unicode => 1,
});

my $teng = Teng::Schema::Loader->load(
    dbh => $dbh,
    namespace => 'MyApp::DB',
);

これを、create-teng-instance.plとして保存し、先ほどのコードを下記のように変更します。

use strict;
use warnings;
use utf8;
use DBI;

my $teng = do('create-teng-instance.pl')
    or die $@;

my $row = $teng->insert(user => {
    id => 1,
    name => 'walf443',
    age  => 26,
});

warn Dumper($row->get_columns);

my $last_insert_id = $teng->fast_insert(user => {
    id => 2,
    name => 'walf444',
    age  => 30,
});

warn $last_insert_id;

Tengのインスタンスを作るところまでは、今後はこのように記述します。

Teng#insert($table, $hashref)は$hashrefの内容を$tableテーブルへ1レコードして追加してくれます。戻り値はTeng::Rowクラスを継承したクラスのインスタンスです。(これを今後はRowオブジェクトと呼びます)

戻り値でRowオブジェクトを返していますが、戻り値でRowオブジェクトを必要としない場合には、Teng#fast_insertが使えます。

実際にデータが挿入されたのか確認してみましょう。

$ sqlite3 quickstart.sqlite3
sqlite> SELECT * FROM user;
1|walf443|26
2|walf444|30

ちゃんとデータが入っていますね。

クエリの確認

Tengでメソッドを実行した際に、どのようなSQLが実行されたのか知りたい、ということはあると思います。

そういうときは色々手段はありますが、現時点ではDBIx::QueryLogを使うのがお手軽かと思います。

perl -MDBIx::QueryLog ./insert-user-fix.pl

実行してみると、思ったよりたくさんのクエリが実行されているのにびっくりされたかもしれません。

これは、ほとんどはTeng::Schema::Loaderにより実行されているクエリです。

Schemaを明示的に定義してやることでこれらのクエリは走らないようにすることもできます。

その方法に関しては後述します。

データの検索

せっかくなので先程の確認の操作をTengからもやってみましょう。

use strict;
use warnings;
use Data::Dumper;

my $teng = do('create-teng-instance.pl')
    or die $@;

my $iter = $teng->search_named(q{
    SELECT * FROM user WHERE ( id IN :ids )
}, { ids => [1, 2] }, 'user');

while ( my $row = $iter->next ) {
    warn Dumper($row->get_columns);
}

Teng#search_named($sql, $hashref, $table_name)は、$sqlをSQLとして実行し、Teng::Iteratorオブジェクトを返します。

SQL内で、:keywordのように記述してあり、$hashrefのキーとして”keyword”が存在していた場合には、:keywordをプレイスホルダに置きかえ、$hashref->{keyword}の値をbindします。 $hashrefの値がArrayRefの場合には、(?,?,?)のように置きかえてくれるため、INの記述が楽です。

$table_nameはTeng::IteratorがRowオブジェクトを生成する際のクラス名を決定するために必要です。

$iterは、Teng::Iteratorオブジェクトです。Teng::Iterator#nextを呼び出すたびに、Rowオブジェクトを生成してくれます。

また、簡単な条件での検索であればTeng#searchメソッドを使うと生でSQLを記述しなくてもよいです。

use strict;
use warnings;
use Data::Dumper;

my $teng = do('create-teng-instance.pl')
    or die $@;

my $iter = $teng->search(user => { id => [1, 2] });
while ( my $row = $iter->next ) {
    warn Dumper($row->get_columns);
}

一件のみ取得する際にはTeng#singleを使うと直接Rowオブジェクトを取得できます。

use strict;
use warnings;
use Data::Dumper;

my $teng = do('create-teng-instance.pl')
    or die $@;

my $user = $teng->single(user => { id => 1 });
warn Dumper($user->get_columns);

データの削除

データを削除する際にはTeng#delete、あるいは、Teng::Row#deleteが使えます。

use strict;
use warnings;
use Data::Dumper;
use Test::More;

my $teng = do('create-teng-instance.pl')
    or die $@;

$teng->fast_insert(user => {
    id => 3,
    name => 'walf445',
    age  => 35,
});

$teng->fast_insert(user => {
    id => 4,
    name => 'walf446',
    age  => 40,
});

my $deleted_num_of_rows = $teng->delete(user => { id => [3, 4] });
is $deleted_num_of_rows => 2, 'deleted_num_of_rows should be 2';

done_testing;

戻り値は、削除された件数です。Teng#deleteは複数件を一気に消すときに便利でしょう。

use strict;
use warnings;
use Data::Dumper;
use Test::More;

my $teng = do('create-teng-instance.pl')
    or die $@;

$teng->fast_insert(user => {
    id => 3,
    name => 'walf445',
    age  => 35,
});

$teng->fast_insert(user => {
    id => 4,
    name => 'walf446',
    age  => 40,
});

my $iter = $teng->search(user => {
    id => [3, 4],
});

while ( my $row = $iter->next ) {
    $row->delete;
}

Rowオブジェクトのdeleteに何かしらのhookを入れているとき(キャッシュの削除等)に使います。 各レコードの削除ごとに1回SQL発行されるので、ケースによってはやや注意して使う必要があるかもしれません。

データの変更

データのupdateもdeleteとほぼ同じです。

use strict;
use warnings;
use Data::Dumper;
use Test::More;

my $teng = do('create-teng-instance.pl')
    or die $@;

$teng->fast_insert(user => {
    id => 3,
    name => 'walf445',
    age  => 35,
});

$teng->fast_insert(user => {
    id => 4,
    name => 'walf446',
    age  => 40,
});

my $iter = $teng->search(user => {
    id => [3, 4],
});

while ( my $row = $iter->next ) {
    $row->update({ age => \"age + 1"});
}

my $walf445 = $teng->single(user => { id => 3 });
is $walf445->age, 36;

my $walf446 = $teng->single(user => { id => 4 });
is $walf446->age, 41;

done_testing;

Rowオブジェクトからupdateした場合、条件が単純であれば元のRowオブジェクトのデータもupdateされていますが、DBに保存されている値と一致していることは保証されないので、取得しなおすのが無難です。

条件にはHashRefだけでなく、ScalarRefを指定することもでき、これらは式や条件を指定する際に便利です。

まとめ

本章では、以下のことについて書きました。

  • Teng#doを使うと任意のSQLを実行することができる
  • Teng::Schema::Loaderを用いると、現在のDBの情報から動的にSchemaを生成してくれる。
  • データを一件入れる際にはTeng#fast_insertあるいはTeng#insertを用いる
  • データを取得する際には、Teng#search_named, Teng#search, Teng#singleを用いる
  • データを削除する際には、Rowオブジェクトのdeleteメソッド、あるいはTeng#deleteを用いる
  • データを変更する際には、Rowオブジェクトのupdateメソッド、あるいはTeng#updateを用いる

やってみよう

  • 他のDB関係のライブラリ(DBI, DBIx::Class等)では、同じような処理をするためにはどのように書くだろうか。
  • 挿入/検索/更新/削除の速度は、他のDB関係のライブラリと比較してどのくらいだろうか。