@nqounetです。
前回はSELECT * FROM usersという最もシンプルなクエリを生成しました。今回はWHERE条件を追加してみます。
WHERE条件を追加したい
実際のアプリケーションでは、全件取得することは稀です。必ずといっていいほどWHERE条件が必要になります。
「じゃあ、コンストラクタに追加しよう」…この安易な発想が、どんな沼への第一歩なのか、ご覧あれ。
Query.pm(WHERE追加版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # 言語: perl
# バージョン: 5.36以上
# 依存: Moo
package Query;
use v5.36;
use Moo;
has table => (is => 'ro', required => 1);
has where_column => (is => 'ro');
has where_value => (is => 'ro');
sub to_sql ($self) {
my $sql = "SELECT * FROM " . $self->table;
if ($self->where_column && defined $self->where_value) {
$sql .= " WHERE " . $self->where_column . " = '" . $self->where_value . "'";
}
return $sql;
}
1;
|
使用例:
1
2
3
4
5
6
7
| my $query = Query->new(
table => 'users',
where_column => 'id',
where_value => 1,
);
say $query->to_sql;
# 出力: SELECT * FROM users WHERE id = '1'
|
動いた!…と思いきや、すぐに次の要望が来ます。
ORDER BYも欲しい
「結果を名前順で並べたいんだけど…」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # 言語: perl
# バージョン: 5.36以上
# 依存: Moo
package Query;
use v5.36;
use Moo;
has table => (is => 'ro', required => 1);
has where_column => (is => 'ro');
has where_value => (is => 'ro');
has order_column => (is => 'ro');
has order_dir => (is => 'ro', default => 'ASC');
sub to_sql ($self) {
my $sql = "SELECT * FROM " . $self->table;
if ($self->where_column && defined $self->where_value) {
$sql .= " WHERE " . $self->where_column . " = '" . $self->where_value . "'";
}
if ($self->order_column) {
$sql .= " ORDER BY " . $self->order_column . " " . $self->order_dir;
}
return $sql;
}
1;
|
パラメータが5個になりました。
LIMITも必要だ
「最新10件だけ取得したいんだけど…」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| # 言語: perl
# バージョン: 5.36以上
# 依存: Moo
package Query;
use v5.36;
use Moo;
has table => (is => 'ro', required => 1);
has where_column => (is => 'ro');
has where_value => (is => 'ro');
has order_column => (is => 'ro');
has order_dir => (is => 'ro', default => 'ASC');
has limit_count => (is => 'ro');
has offset_count => (is => 'ro');
sub to_sql ($self) {
my $sql = "SELECT * FROM " . $self->table;
if ($self->where_column && defined $self->where_value) {
$sql .= " WHERE " . $self->where_column . " = '" . $self->where_value . "'";
}
if ($self->order_column) {
$sql .= " ORDER BY " . $self->order_column . " " . $self->order_dir;
}
if ($self->limit_count) {
$sql .= " LIMIT " . $self->limit_count;
if ($self->offset_count) {
$sql .= " OFFSET " . $self->offset_count;
}
}
return $sql;
}
1;
|
7個になりました。使う側はこうなります:
1
2
3
4
5
6
7
8
9
| my $query = Query->new(
table => 'users',
where_column => 'status',
where_value => 'active',
order_column => 'created_at',
order_dir => 'DESC',
limit_count => 10,
offset_count => 0,
);
|
これがTelescoping Constructorアンチパターン
これは「Telescoping Constructor」と呼ばれるアンチパターンです。
問題点:
- パラメータの順序を覚えきれない
- 同じ型(文字列)が複数あり、間違えやすい
- 必須と任意の区別がつきにくい
- 新しいパラメータを追加するたびに、既存のコードに影響が出る
しかも、まだ足りない機能があります:
- 複数のWHERE条件(AND/OR)
- 特定のカラムだけ取得(SELECT id, name FROM…)
- JOIN句
- GROUP BY / HAVING
これらを全部コンストラクタに追加したら、パラメータは20個を超えてしまいます。
SOLID原則から見た問題
この設計はSOLID原則の「SRP(単一責任原則)」に違反しています。
Queryクラスは以下のすべてを担当しようとしています:
- テーブル名の管理
- WHERE条件の管理
- ORDER BYの管理
- LIMITの管理
- SQLの生成
1つのクラスが「すべてのSQL句の管理とSQL生成」という複数の責任を負っています。パラメータが増え続けるのは、責任が集中しているサインなのです。
今回のまとめ
今回はWHERE、ORDER BY、LIMITを追加しようとして、パラメータ地獄に陥りました。
- 機能追加のたびにパラメータが増殖
- 7個のパラメータでも使いにくい
- これ以上の拡張は現実的ではない
この問題を「Telescoping Constructor(望遠鏡コンストラクタ)」アンチパターンと呼びます。コンストラクタの引数がどんどん伸びていく様子が望遠鏡のように見えるからです。
次回は、さらに深刻な問題を紹介します。このコードにはセキュリティホールがあります。SQLインジェクション攻撃を実際に試してみましょう。