DBICが発行するSQLのテーブル名をクォートさせたい

※解決しました。フォローエントリ → 続・DBICが発行するSQLのテーブル名をクォートさせたい

hoges, conditionsというテーブルがあって、hoges_conditionsを中間テーブルとしてmany_to_manyの関係を持たせた時にハマった。

create table `hoges_conditions` (
    `hoge`           int not null,
    `condition`      int not null,
    `created_at`     datetime not null,
    `updated_at`     timestamp not null,
    foreign key(`hoge`)      references hoges(id)      on delete cascade,
    foreign key(`condition`) references conditions(id) on delete cascade
) engine=innodb;

「condition」がMySQL予約語なので、バッククォートでくくらないとエラーになる。

MyApp-Web/lib/MyApp/Schema/Hoges.pm で

__PACKAGE__->many_to_many('conditions' => 'hoges_conditions', 'condition');

と定義しておいて、

$c->model('DB::Hoges')->search;

とか呼ぶと、以下のようなSQLを吐いて死ぬ。

SELECT COUNT( * ) FROM hoges_conditions me JOIN conditions condition ON ( condition.id = me.condition ) WHERE ( me.hoge = ? )

Wassrで助けを求めたところ、

$self->model_slave('Hoge')->proxy('`hoge`')->single();

DBICの奥底のresult_source->schema->source_registrations-> {$self->result_source->source_name}->name($table) をさしかえてやればいけるかと

とnekokakさんからアドバイスもらえた。けど、relation先のテーブルの別名なので今回のケースには適合しなさそう。

あとは、

$schema->storage->dbh->do("SELECT * FROM `foo`;");

こんな解決策がWassrの中の人から提示されましたが、激しくネタです。本当にありがとうございました。

ちょっと見てみると、JOIN句のSQLDBIx::Class::Storage::DBIが作ってるっぽい。テーブルに別名を付けてる部分は235行目あたりか。

sub _make_as {
  my ($self, $from) = @_;
  return join(' ', map { (ref $_ eq 'SCALAR' ? $$_ : $self->_quote($_)) }
                     reverse each %{$self->_skip_options($from)});
}

mapの部分を

  return join(' ', map { (ref $_ eq 'SCALAR' ? $$_ : '`'.$self->_quote($_).'`') }

てな具合に変更。

SELECT condition.id, (略) FROM `hoges_conditions` `me`  JOIN `conditions` `condition` ON ( condition.id = me.condition ) WHERE ( me.hoge = ? ): '1'

こんな具合になって無事に通りました。

ひとまずこれで手元の問題は解決したんだけど、やっぱり「そもそも予約語を使わない」のが正解なんだろうな。何か適当な名前に変えた方が無難かなー。

以前はこういう場面だと、

  • テーブル名は「conditions」
  • 外部キーは「condition_id」

にしてたんだけど、DBICが「$hoge->(外部キー名)」でそのまま対象オブジェクトが取得できるから「condition_id」みたいな名前にするより直接「condition」とか書くようになりました。
使うライブラリでDBの設計が変わるのもどうかなと思わなくもないけど、まぁそんなもんだよね。

その後さらにnekokakさんから、クォートさせたいクラスで

__PACKAGE__->table("`foo`");

とか書けばいいんじゃね? というアドバイスが。これだと「JOIN `foo` bar ON…」となって今回は後ろのテーブル名をクォートさせる必要があったのでクリアできなかったけど、これは使う機会がありそうなので憶えておこう。

今回に限らず、Perlで困った時にWassrでつぶやくと素敵なお兄さんがアドバイスをくれることが多いので、皆もWassrやるといいよ!

# 以下、追記。
nekokakさんからこんなコメントを頂いた。

うちの場合、リレーションはcondition_idにしておいて、conditionでもアクセスできるようにSchema::Hoge::condition_idのcoderefをSchema::Hoge::conditionにぶちこんでます

coderefをどう扱えばいいのか、今ひとつ理解してなかったけど、外部キーはcondition_idで定義しておいてHoges.pmで

*condition = \&condition_id;

しておけば$hoge->condition->searchとかできるわけか。素晴らしい。nekokak++

「まぁ滅多なことでは必要ないだろ」とか思って、型グロブをスルーしていたから分からなかったんだな。基本は大切。反省。

そして、そもそもmany_to_manyの場合はリレーションの名前はModel内で宣言するだけなので、外部キーの名前は何でもいいことに今になって気付いた。つまり、

__PACKAGE__->many_to_many('conditions' => 'hoges_conditions', 'condition_id');

としておけば、$hoge->conditionsで取れる。あほか俺。