いまどきのカジュアルなデータベース関連開発

YAPC::Asia 2013

Sep 20th, 2013

profile

songmu

今年新たに上げたCPANモジュール

あと

をtypesterの代理でCPANに上げました

アジェンダ

DB開発ってことですが

ORM

Teng and DBIx::Class

typester likes DBIC.

社内では、去年Tengに傾いたものの、今年に入って揺り戻しがあって、 DBICが増えてる。新規でTeng使ってるの多分僕だけ><

僕もDBICきらいじゃないので、結構ゴツメの使い方でTengを使っている

開発用モジュールの構成

スキーマ(DDL)管理 - use DBIx::Schema::DSL;

拙作。自分でSQLかけばええやんて感じだけど+αしてる

SQL::Translatorに対するDSL的な感じ。

定義

package MyApp::Schema;
use DBIx::Schema::DSL;

create_table player => columns {
    primary_key 'id';
    varchar     'name';
};

出力

% perl -ML -E 'say MyApp::Schema->output' > sql/myapp.sql

DBIx::Schema::DSL

メリット

デメリット

何故スキーマ管理にMySQL Workbenchとかを使わないか

Migration - use GitDDL::Migrator;

GitDDL(::Migrator)の仕組み

Tengのスキーマ定義 - use Teng::Schema::Loader;

partition切ってて、pkが id, created_atの時にちょっとおかしくなった時があったので 無理やり定義書き換えてidのみをpkに定義しなおしたりとかしてる。詳しいことは忘れた

JOIN - use Teng::Plugin::SearchJoined;

TengはJOINサポートしてない割り切った設計になっている(agree)、が joinしたSQL投げつつ、rowオブジェクトは個別に取るみたいなことはやりたい

my $itr = $teng->search_joined(
    user_item => [
    item       => {'item.id' => 'user_item.item_id'},
], {
    'user_item.user_id' => 10,
});
# $itr->suppress_object_creation(1);
while (my ($user_item, $item) = $itr->next) {
    ...
}

ResultSet - use MyApp::DB::ResultSet;

DBICだと、Rowの集合みたいな概念があって結構便利。DBIC脳だと欲しくなる

こんな感じ

my $rs = $teng->result_set('player');
$rs->single({id => 1});

これは以下とほぼ等価

$teng->single(player, {id => 1});

Teng::Plugin::ResultSet でも書こうかと思ってるけど保留中

例外処理 - use Exception::Tiny;

トランザクションとの連携 - DBIx::TransactionManager::EndHook

use DBIx::TransactionManager::EndHook;
sub take_item {
    my ($self, $item) = @_;
    $self->items->add($item);
    $teng->transaction_manager->add_end_hook(sub {
        log('take item!');
    });
}

MySQL以外のミドルウェア

Redis

Redisの注意点

Redis.pm

Cache::Redis

拙作

Redis::LeaderBoard

拙作

Fluent::Logger

DB設計

テーブル定義

割と普通。

インデックスは必要最低限

外部キー張らない

接続時の処理

on_connect_do => [
    "SET NAMES utf8mb4",
    "SET SESSION sql_mode='TRADITIONAL'",
],

たまに勘違いしてる人がいるけど(ぼくもしてた)、on_connect_doはDBI標準の機能ではない。DBIで同様のことをしたい場合は、Callbacksを使う。

Unicode6とutf8mb4

パーティショニング

MySQL5.1以降

マスターデータの考え方

DBIx::FixtureLoader

拙作。フィクスチャをよしなにDBに流しこんでくれる。[テーブル名].[拡張子]

[テーブル名]-1.[拡張子] のようにハイフン区切りでpostfixをつけておくことも可能。

論理削除を使わない

MVC

MVCとはなんだったのか

荒れるので略

karupanerura君が話すそうです

ぼくがかんがえたさいきょうのMVC

ディレクトリ構成

Model/ とは別に DB/ を用意。ORM関連は DB/ に置く。

lib/MyApp/
├── CLI
├── Controller
├── DB
├── Model
└── View

Model

を担当する。

ORMの考え方

Test

Test::mysqld

事前データ作る

use Path::Tiny qw/path/;
use Test::mysqld;
sub prepare_mysqld_copy_data {
    my $datadir = 'tmp/test_mysqld_data';
    path($datadir)->rmtree;

    my $mysqld = Test::mysqld->new(
        base_dir => 'tmp/test_mysqld',
        my_cnf   => {
            datadir => $datadir, # $datadirを指定
            'skip-networking' => '',
        },
    ) or die $Test::mysqld::errstr;

    # データ流し込み
    deploy_test_database($mysqld->dsn);
}

データ流し込み

use DBI;
use DBIx::FixtureLoder;
sub deploy_test_database {
    my $dsn = shift;
    my $dbh = DBI->connect($dsn);

    # スキーマ流し込み
    my $source = path('sql/myapp_ddl.sql')->slurp;
    for my $stmt (split /;/, $source) {
        next unless $stmt =~ /\S/;
        $dbh->do($stmt) or die $dbh->errstr;
    }

    # マスタデータ流し込み
    my $loader = DBIx::FixtureLoder->new(dbh => $dbh);
    for my $file (path('data/master')->children) {
        $loader->load_fixture($file);
    }
}

→ tmp/test_mysqld_data にデータディレクトリが作られる。

実際テストを走らせる

my $mysqld = Test::mysqld->new(
    copy_data_from => 'tmp/test_mysqld_data',
    my_cnf         => { 'skip-networking' => ''},
) or die $Test::mysqld::errstr;

マスタデータとテスト

Test::mysqlのテスト以外使い途

(Tips)発行されているSQLをテストする

# こんなかんじのやつをt::Utilに定義
sub trace_sqls(&) {
    my $code = shift;
    my @sqls;
    require DBIx::Tracer;
    my $tracer = DBIx::Tracer->new(sub {
        my %args = @_;
        push @sqls, $args{sql};
    });
    $code->();
    @sqls;
}

# こんなかんじで使う
my @sqls = trace_sqls {
    $dbh->do('UPDATE ...');
    ...
};

Profiling

基本的には

去年のトークでfujiwaraさんが話していたようなことです。 アタリマエのことだけど、だがそれが難しい。

DBIx::QueryLog

Devel::KYTProf

canでcoderef取ってフックしてってやってるので、AUTOLOAD使ってるRedisの場合だと事前に

eval {$redis->$_} for @commands;

とかやって登録しておかないとダメ。

IOネックなやつはだいたいこのへんで潰せる。

Devel::NYTProf

アプリネックな場合でにっちもさっちも行かなくなったら使う。

だいたいDBICとかDateTimeが原因でしょんぼりする。

なので僕はDBICとDateTimeは使っておらず、DBICの代わりにTeng。DateTimeの代わりにTime::Piece::Plus

最後に

isucon

ISUCON3

挑戦受付中

https://isucon2013.kayac.com/

もう一つ

lobi

We are hiring!

lobi

以上

ありがとうございました。

質問おねがいします!