Inside of DBIx::Schema::DSL
Shibuya.pm #18
Jul 5th, 2018
Profile
Mackerel
Mackerel
- はてな謹製のサーバー管理・監視SaaS
- 小規模から大規模システムまで
- ユーザー登録から3分でサーバー監視が可能
- 専属UI/UXデザイナーによる圧倒的な使い勝手
- 200週連続機能リリース達成しました
【宣伝】書籍発売中
【宣伝】みんなのGo言語
DBIx::Schema::DSLの話
先程バージョン 1.0000 をshipしました
DBIx::Schema::DSL
package MySchema;
use DBIx::Schema::DSL;
database 'MySQL';
create_table author => columns {
integer 'id', primary_key, auto_increment;
varchar 'name';
integer 'age';
decimal 'height', precision => 4, scale => 1;
add_index 'height_idx' => ['height'];
has_many 'book';
};
create_table book => columns {
primary_key 'id';
varchar 'name';
integer 'author_id';
decimal 'price', size => [4,2];
column 'meta', 'json';
add_index 'author_id_idx' => ['author_id'];
belongs_to 'author';
};
出力
% perl -ML -E 'say MySchema->output' > sql/myapp.sql
DBIx::Schema::DSL
- DSLでテーブル定義を書けるやつ
- スキーマの生成等に便利
- 内部的にはSQL::Translatorのオブジェクトを生成している
- Perl製ORMであるAnikiの内部でも利用されている
メリデメ
メリット
- Perlの定数連携(Default値とか)
- DRYに書ける
- Perlなのでシンタックスチェックが楽(?)
- SQLのクオリティーが統一される
- 特にSQL::TranslatorフレンドリーなSQLが出力される
デメリット
SQL::Translator
SQL::Translator is a group of Perl modules that manipulate structured data definitions (mostly database schemas)
SQL::Translator
主にSQLスキーマを読み込んで、Perlのオブジェクトにマッピングし、それを様々な形で出力可能にするすごいやつ。
SQL::Translator::Parser::*
↓
SQL::Translator
↓
SQL::Translator::Producer::*
SQL::Translator::Parser
- SQL::Translator::Parser::Access - parser for Access as produced by mdbtools
- SQL::Translator::Parser::DB2 - parser for DB2
- SQL::Translator::Parser::DBI - "parser" for DBI handles
- SQL::Translator::Parser::DBI::DB2 - parser for DBD::DB2
- SQL::Translator::Parser::DBI::MySQL - parser for DBD::mysql
- SQL::Translator::Parser::DBI::Oracle - parser for DBD::Oracle
- SQL::Translator::Parser::DBI::PostgreSQL - parser for DBD::Pg
- SQL::Translator::Parser::DBI::SQLServer - parser for SQL Server through DBD::ODBC
- SQL::Translator::Parser::DBI::SQLite - parser for DBD::SQLite
- SQL::Translator::Parser::DBI::Sybase - parser for DBD::Sybase
- SQL::Translator::Parser::Excel - parser for Excel
- SQL::Translator::Parser::JSON - Parse a JSON representation of a schema
- SQL::Translator::Parser::MySQL - parser for MySQL
- SQL::Translator::Parser::Oracle - parser for Oracle
- SQL::Translator::Parser::PostgreSQL - parser for PostgreSQL
- SQL::Translator::Parser::SQLServer - parser for SQL Server
- SQL::Translator::Parser::SQLite - parser for SQLite
- SQL::Translator::Parser::Storable - parser for Schema objects serialized with the Storable module
- SQL::Translator::Parser::Sybase - parser for Sybase
- SQL::Translator::Parser::XML - Alias to XML::SQLFairy parser
- SQL::Translator::Parser::XML::SQLFairy - parser for SQL::Translator's XML.
- SQL::Translator::Parser::YAML - Parse a YAML representation of a schema
- SQL::Translator::Parser::xSV - parser for arbitrarily delimited text files
SQL::Translator::Producer
- SQL::Translator::Producer::ClassDBI - create Class::DBI classes from schema
- SQL::Translator::Producer::DB2 - DB2 SQL producer
- SQL::Translator::Producer::DiaUml
- SQL::Translator::Producer::Diagram - ER diagram producer for SQL::Translator
- SQL::Translator::Producer::Dumper - SQL Dumper producer for SQL::Translator
- SQL::Translator::Producer::GraphViz - GraphViz producer for SQL::Translator
- SQL::Translator::Producer::HTML - HTML producer for SQL::Translator
- SQL::Translator::Producer::JSON - A JSON producer for SQL::Translator
- SQL::Translator::Producer::Latex
- SQL::Translator::Producer::MySQL - MySQL-specific producer for SQL::Translator
- SQL::Translator::Producer::Oracle - Oracle SQL producer
- SQL::Translator::Producer::POD - POD producer for SQL::Translator
- SQL::Translator::Producer::PostgreSQL - PostgreSQL producer for SQL::Translator
- SQL::Translator::Producer::SQLServer - MS SQLServer producer for SQL::Translator
- SQL::Translator::Producer::SQLite - SQLite producer for SQL::Translator
- SQL::Translator::Producer::Storable - serializes the SQL::Translator::Schema object via the Storable module
- SQL::Translator::Producer::Sybase - Sybase producer for SQL::Translator
- SQL::Translator::Producer::TT::Base - TT (Template Toolkit) based Producer base class.
- SQL::Translator::Producer::TT::Table
- SQL::Translator::Producer::TTSchema
- SQL::Translator::Producer::XML - Alias to XML::SQLFairy producer
- SQL::Translator::Producer::XML::SQLFairy - SQLFairy's default XML format
- SQL::Translator::Producer::YAML - A YAML producer for SQL::Translator
SQL::Translator::Producer::Teng
DEMO
PerlによるDSLの作り方
DSLとは?
- Domain Specific Language
- 特定用途向けの独自言語
- XMLとかYAMLとかもDSLの一種といえる
- LLで書くDSL
- Chef/Puppuet/Rake
- cpanfile
Perlで作られたDSL
- Plack::Builder
- Moose/Mouse/Moo
- Module::CPANFile / cpanfile
- Web::Scraper
- Daiku / Daikufile
- DBIx::Schema::DSL
LLで書くDSLの種類
- 言語内DSL (言語の文法を拡張する)
- 言語外DSL (独自文法)
言語内DSLの種類
- コードを書くために一時的に文法を拡張するもの(シンタックスシュガーの定義)
- Moose/Mouse/Moo
- Try::Tiny
- 限定用途
- cpanfile
- Daikufile
- DBIx::Schema::DSL
Perlによる言語内DSLの作り方
- シンタックスは関数として定義する
- サブルーチンプロトタイプの活用
DSLのシンタックスは関数として定義する
- 裸のワードは基本的には関数である
- 逆にbarewordは関数だと理解できれば読み書きもしやすくなる
- Perlは関数呼び出しのパーレンを省略できるので関数をキーワードっぽく使用できる
- モジュールをuseすると関数をexportするようにしておく
サブルーチンプロトタイプ
sub名の後にparenを付けて引数の型を指定することができる機能だが、今日ではほとんど使われないし使わないほうが良い。現代では、Perlにもsubroutine signatureがある。
DSLを作るときに活用されるという本来の目的とはちょっと違う用途に使われることがある。
サブルーチンプロトタイプの実例
sub CONST() { 'CONST' } # 定数
sub hoge($) {
my $scalar = shift;
}
sub run(&) {
my $code = shift;
$code->();
}
コードブロックを受け取るプロトタイプ
sub run(&) {
my $code = shift;
$code->();
}
run( sub { ... });
# を以下のように書ける
run {
... # ここのコードが実行される
};
DBIx::Schema::DSLの場合
create_table book => columns {
integer 'id', primary_key, auto_increment;
varchar 'name', null;
integer 'author_id', not_null;
decimal 'price', 'size' => [4,2];
column 'meta', 'json';
add_index 'author_id_idx' => ['author_id'];
belongs_to 'author';
};
ここで使われているシンタックス一覧
sub create_table($$)
sub columns(&)
sub column($$;%)
sub varchar
sub integer
sub primary_key
sub auto_increment
sub not_null
sub add_index
sub belongs_to
状態の保存
単なる関数呼び出しなのにどこに状態を保存しているのか
caller
を使った呼び出し元パッケージ解決
- 呼び出し元パッケージのクラス変数にオブジェクトを隠す
- gotoの活用
callerを使った呼び出し元解決
my $pkg = caller;
- その関数の呼び出し元のパッケージ名が返る
- そのパッケージのクラス変数に状態を保存
クラス変数にコンテキストオブジェクトを隠す
sub contex {
my $pkg = shift;
die 'something went wrong when calling context method.' if $pkg eq __PACKAGE__;
no strict 'refs';
${"$pkg\::CONTEXT"} ||= DBIx::Schema::DSL::Context->new;
}
my $c = caller->context;
というコードが頻出する。
goto &NAME
いくつかあるgoto。perldoc -f goto
参考のこと
The "goto &NAME" form is quite different from the other forms of
"goto". In fact, it isn't a goto in the normal sense at all, and
doesn't have the stigma associated with other gotos. Instead, it
exits the current subroutine (losing any changes set by "local")
and immediately calls in its place the named subroutine using the
current value of @_. This is used by "AUTOLOAD" subroutines that
wish to load another subroutine and then pretend that the other
subroutine had been called in the first place (except that any
modifications to @_ in the current subroutine are propagated to
the other subroutine.) After the "goto", not even "caller" will be
able to tell that this routine was called first.
NAME needn't be the name of a subroutine; it can be a scalar
variable containing a code reference or a block that evaluates to
a code reference.
つまり?
- callスタックを積まずに関数を呼び出す
- あたかも上位層から直接呼び出されたかのように見せかけることができる
呼び出し元を変えないためにgotoを活用
integer 'id';
# 内部的には以下を呼び出している
column 'id, 'integer';
columnの定義は以下のような感じ
sub column($$;%) {
my ($column_name, $data_type, @opt) = @_;
...
普通の関数呼び出しではダメ
sub integer {
my $column_name = shift;
column $column_name, 'integer', @_;
}
caller
が書き換わってしまう
- DBIx::Schema::DSLパッケージ内呼び出しになってしまう
gotoを使う
引数(@_
)を書き換えてから gotoで関数を呼び出す。
sub integer {
my $column_name = shift;
@_ = ($column_name, 'integer', @_);
goto \&column;
}
これであたかもcolumn
関数が呼び出し元のパッケージから直接呼び出されたかのようになる。
実際にintegerからcolumnを呼び出しているところ
for my $method (@column_methods) {
no strict 'refs';
*{__PACKAGE__."::$method"} = sub {
use strict 'refs';
my $column_name = shift;
@_ = ($column_name, $method, @_);
goto \&column;
};
}
頑張ってますね。
ちょいネタ
void contexの判定
sub column($$;%) {
my ($column_name, $data_type, @opt) = @_;
croak '`column` function called in non void context'
if defined wantarray;
wantarrayの返り値
- undef: void
- 0: スカラコンテキスト
- 1: リストコンテキスト
以下のような呼び出しを避けられる
# 普通に代入
my $var = column ...;
# columns column の間違い
create_table 'hoge', column { # s/column/columns/g
...
};
# 行末のミス
column ..., # <- セミコロンじゃなくてカンマになってる
column ...;
関数呼び出し位置の制限
column
, integer
などの呼び出しは、create_table
内のみ呼び出せるように制限
- 変なところで呼び出すと例外が上がる
言語内DSLとエラーメッセージ
- 言語内DSLはそもそも言語の柔らかい文法を逆手に取って記法を拡張している
- 意図しない書き方ができてしまうことも
- ユーザーに適切にヒントを与えるためにも、変な呼び出しを検知してエラーを上げるのは大事