雑なデータ加工に使えるORM:Otogiri

※このエントリはPerl Advent Calendar 2023の3日目の記事です。

※元々は「雑なデータ加工に使える小技5撰」でしたが内容がOtogiriだらけなので、タイトルを変更しました。

Otogiriとは

拙作のPerl製ORM(もどき)です。Otogiri - CPANを参照してください。

特徴として、以下のようなものがあります。

  • スキーマ定義がない(DB/テーブルにあるものを正としている)
  • データは全てハッシュリファレンスかその配列で表現される
  • 端的に言えば DBIx::Sunny + SQL::Maker

MetaCPANに公開されているモジュールですので、cpanm Otogiriでインストールできます。

Otogiriの基本的な使い方

DB接続

podにも書いてありますが、DB接続は以下のようにして行います。以下の例ではmysqlに接続しています。

1
2
3
4
5
6
use Otogiri;
my $db = Otogiri->new(connect_info => [
'dbi:mysql:dbname=mydb',
'dbuser',
'dbpassword'
]);

strictモード

Otogiriはデフォルトでstrictモードで動作します。strictモードはSQL::Makerのstrictモードそのままの制約がつきます。

strictモードはこちらに記載があるようなSecurity Issue(JSON SQL Injection)への対策として、SQL::Makerに実装されています。もしWebアプリケーションにおいて、ユーザーからの入力をSQLに埋め込む場合はstrictモードを有効にすることを強くお勧めします。

strictモードを解除するには、DB接続時に以下のようにします。

1
2
3
4
5
6
7
8
my $db = Otogiri->new(
connect_info => [
'dbi:mysql:dbname=mydb',
'dbuser',
'dbpassword'
],
strict => 0
);

これはselectやinsertの際の記述方法に影響があります。別途、selectやinsertの説明の際に触れます。

データ取得

データ取得はselectメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## strictモードが有効の場合
my @rows = $db->select(user => { id => sql_in([1, 4]) });

## strictモードが無効の場合
my @rows = $db->select(user => { id => [1, 4] });

## 以下のようなデータが@rowsに入ります。
## (
## {
## id => 1,
## name => 'ytnobody',
## age => 43,
## created_at => '2023-12-03 00:00:00'
## },
## {
## id => 4,
## name => 'somebody',
## age => 36,
## created_at => '2023-12-03 01:00:00'
## },
## )

単一データを取得する場合はfetchメソッドを使います。

1
2
3
4
5
6
7
8
9
my $row = $db->fetch(user => { id => 1 });

## 以下のようなデータが$rowに入ります。
## {
## id => 1,
## name => 'ytnobody',
## age => 43,
## created_at => '2023-12-03 00:00:00'
## }

データ挿入

データ挿入はinsertメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
## strictモードが有効の場合
$db->insert(user => {
name => 'ytnobody',
age => 43,
created_at => sql_raw("datetime(now)"),
});

## strictモードが無効の場合
$db->insert(user => {
name => 'ytnobody',
age => 43,
created_at => \"datetime(now)",
});

データ更新

データ更新はupdateメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
## strictモードが有効の場合
$db->update(user => {
age => 44,
created_at => sql_raw("datetime(now)"),
}, { id => 1 });

## strictモードが無効の場合
$db->update(user => {
age => 44,
created_at => \"datetime(now)",
}, { id => 1 });

データ削除

データ削除はdeleteメソッドを使います。

1
2
3
4
5
## strictモードが有効の場合
$db->delete(user => { id => sql_in([1, 4]) });

## strictモードが無効の場合
$db->delete(user => { id => [1, 4] });

トランザクション

トランザクションはtxn_scopeメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
12
my $txn = $db->txn_scope;
$db->insert(user => {
name => 'ytnobody',
age => 43,
created_at => sql_raw("datetime(now)"),
});
$db->insert(user => {
name => 'somebody',
age => 36,
created_at => sql_raw("datetime(now)"),
});
$txn->commit;

もっと複雑な使い方

ここから先はさらに複雑な使い方を紹介します。

データ取得時にカラム指定を行う

Otogiriは基本的にテーブルにある全てのカラムを取得します。これは「雑にデータ取得と処理を行うこと」を主目的としているため、そのままではカラム指定を行うことができません。

ですが、Otogiriにはプラグイン機構があります。これはOtogiri::Pluginというモジュールによって実現されています。

カラム指定を行うプラグインはOtogiri::Plugin::SelectWithColumnsです。

使い方は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
use Otogiri;
use Otogiri::Plugin;
my $db = Otogiri->new(connect_info => [...]);
$db->load_plugin('SelectWithColumns');

my @rows = $db->select_with_columns(
'some_table',
['id', 'name'],
{'author' => 'ytnobody'},
{order_by => 'id ASC'}
);

select_with_columns というメソッドが使えるようになり、その第2引数でカラムを指定することができます。

スキーマ定義をしてデータクラスを指定する

Otogiriの特徴には「スキーマ定義がない」というものがありましたが、その気になればスキーマ定義を書くこともできます。

perl-5.38以降であれば class 構文を使うことでスキーマ定義を書くことができます。

1
2
3
4
5
6
7
8
9
10
11
12
class Book {
field $id :param;
field $title :param;
field $author :param;
field $price :param;
field $created_at :param;
field $updated_at :param;

method title {
return $title;
}
};

実際に単一取得を行う場合、事前にrow_classメソッドでスキーマ定義を指定することで、スキーマ定義に従ったオブジェクトを取得することができます。

1
2
my $book = $db->row_class('Book')->single(book => {id => 1}); 
say $book->title;

敢えてrow_classを無効にしたいシーンでは、row_classメソッドの代わりにno_row_classメソッドを使います。

1
2
my $book = $db->no_row_class->single(book => {id => 1});
say $book->{title};

inflate/deflate

Otogiriの機能で最も複雑なのがinflate/deflateです。これはDBから取得したデータを予め指定した関数に基づいて変換したり、DBに格納するデータを予め指定した関数に基づいて変換したりする機能です。

inflate/deflateは以下のようにして使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use JSON;
use Otogiri;
my $db = Otogiri->new(
connect_info => [...],
inflate => sub {
my ($data, $tablename, $db) = @_;
if (defined $data->{json}) {
$data->{json} = decode_json($data->{json});
}
$data->{table} = $tablename;
$data;
},
deflate => sub {
my ($data, $tablename, $db) = @_;
if (defined $data->{json}) {
$data->{json} = encode_json($data->{json});
}
$data;
},
);

inflateはselect(), search_by_sql(), fetch()がコールされた後に呼ばれ、取得データを加工します。
deflateはinsert(), update(), delete()がコールされる前に呼ばれ、格納データを加工します。

直近のOtogiriについて

基本的にはOtogiriのメンテははっきり言ってのんびりしています。ですが、row_class の機能は最近追加されたばかりです。

また先ほど、DBURLへ対応をさせるためのPull-Requestを作成したところです。

もしこれが取り込まれたら、以下のようにDB接続情報をURLで指定できるようになります。

1
2
my $dburl = 'mysql://dbuser:dbpassword@dbhost/mydb?someOption=someValue&otherOption=otherValue';
my $db = Otogiri->new(dburl => $dburl);

Otogiriの今後

基本的には「雑にデータ取得と処理を行うこと」が主目的なのは変わらずで、そのための機能を追加していく予定です。

ぜひ、Otogiriを使ってみてください。そして、Pluginの作成やPull-Requestの作成など、Otogiriの開発にも参加していただけると嬉しいです。