Has Many関係をSelectする際にクエリ分割したほうが良い場合

サービスイン前だからデータが入ってないカラムに対してひたすらクエリを書く 実行結果の確認作業は行えないので雰囲気で書いている

前提

  • PHPでの話。
  • mysqlでの話。
  • 次のテーブル構造での話
    • Subjects --(1:n)-+ Subjects_Tags +-(n:1)-- Tags
  • PHPから突っ込む部分は抽象的に示す。
  • PHPにてSelect結果は(連想)配列で得られるものとする。

本題

Has Many関係にあるデータをSelectする際、いくつか取れる手段はあるが、Has Manyにあたるテーブルのレコード取得は別途SQLを発行するようにしたい。

一回のクエリで済ませる場合

以下のクエリでSubject主体にデータ構造を纏める場合、PHP側のコードでいくらか整形することになるので、やだなーと。

SELECT S.name, T.name AS tag_name
FROM Subjects_Tags AS S_T
  LEFT JOIN Subjects AS S ON S_T.subject_id = S.id
  LEFT JOIN Tags ON S_T.tag_id = Tags.id
WHERE S_T.subject_id = :id
;
// subject1件だけに注目してselect
$records = $db->query(/* 省略 */);
// // 常軌を逸した手抜き
// $subject = $records[0];
// $subject['tags'] = $records;

// 最低限の誠実対応
$subject = [];
// カラムがある程、同様のステップが増える
$subject['name'] = $records[0]['name'];

$tag_records = array_map(function($record){
    // 別名をつけたカラムについて、本来の名称に戻す
    // Subjectと名前が被ったカラムがある程、同様のステップが増える
    $record['name'] = $record['tag_name'];
    unset($record['tag_name']);
    // Subjectにのみ存在するカラム名を消す
    // カラムがある程、Subjectのカラムをunsetするステップが増える
    // unset($record['subject_only_column'])

    return $record;
}, $records);
$subject['tags'] = $tag_records;

Subjectテーブルのカラム名だけをピックアップしたり、tagとしてレコードを持つために別名つけたカラムを戻したりと、まぁまぁだるい。

いやまぁ整形しなくてもやりようはあるけど、やりたくない。

暗黒面

そう、やりようはある。PHPerは、やる。

<? // 常軌を逸した手抜きをしたケース?>
<div>
    <h3>name:<?php echo $records[0]['name'] // 1件しか参照しないから先頭要素の名前みればいいじゃーん ?></h3>
    <h4>Tags</h4>
    <ul>
    <?php foreach($records as $record): ?>
        <li><?php echo $record['tag_name'] // あのSQL使うときはタグ名は別名になるの知ってるからokok ?></li>
    <?php endforeach; ?>
    </ul>
</div>

↑みたいなSubject一件だけしかない時にできる雑を。

-- Subjectも複数件ほしい
SELECT S.id, S.name, T.name AS tag_name, T.id AS tag_id
FROM Subjects_Tags AS S_T
  LEFT JOIN Subjects AS S ON S_T.subject_id = S.id
  LEFT JOIN Tags ON S_T.tag_id = Tags.id
WHERE S_T.subject_id IN ( /* id埋め込み */ )
ORDER BY S.id, T.id
;
<?php $lastSubjectId = null; ?>
<div>
<?php foreach($records as $record): ?>
    <?php if ($lastSubjectId === null):>
    <h3>name:<?php echo $record['name'] ?></h3>
    <h4>Tags</h4>
    <ul>
    <?php elseif ($lastSubjectId !== $record['id']):>
    </ul>
</div>
<div>
    <h3>name:<?php echo $record['name'] ?></h3>
    <h4>Tags</h4>
    <ul>
    <?php endif;>
    <li><?php echo $record['tag_name'] ?></li>
<?php endforeach; ?>
<?php if ($lastSubjectId !== null):>
    </ul>
<?php endif;>
</div>

こういうことを、やる。

分割すれば少しは苦痛が和らぐ

冗長なので複雑性のある、Subject複数件取得で、関連するTagも持ってくるパターンだけ。

SELECT *
FROM Subjects
WHERE id IN ( /* id埋め込み */ )
;

SELECT *
FROM Subjects_Tags as S_T
  LEFT JOIN Tags ON S_T.tag_id = Tags.id
WHERE S_T.subject_id IN ( /* id埋め込み */ )
;
$s_records = $db->query(/* 省略 */);
// [id => record] の構造に変換
$subjects = [];
foreach($s_records as $record){
    $record['tags'] = [];
    $subjects[$record['id']] = $record;
}

$t_records =  $db->query(/* 省略 */);
foreach($t_records as $record) {
    if (empty($subjects[$record['subject_id']])) {
        // Updateが挟まれなければ滅多にcontinueされない
        continue;
    }
    $subjects[$record['subject_id']]['tags'][] = $record;
}
<?php foreach($subjects as $subject): ?>
<div>
    <h3>name:<?php echo $subject['name'] ?></h3>
    <h4>Tags</h4>
    <ul>
    <?php foreach($subject['tags'] as $tag): ?>
        <li><?php echo $tag['name'] ?></li>
    <?php endforeach; ?>
    </ul>
</div>
<?php endforeach; ?>
よいところ
  • 被りやすいカラム名の別名を用意する必要がなくなる
  • レコードから構造化データへの変換コストが減る
  • 構造化データを利用しやすくすることで、テンプレート部分の可読性が向上する 重要
よくないところ
  • クエリ分割によって、ひとかたまりのデータ塊ではなくなるので、分割前とくらべてデータが不安定になる。
  • 分割した分のオーバーヘッドが発生する
[紹介元] PHPタグが付けられた新着投稿 – Qiita Has Many関係をSelectする際にクエリ分割したほうが良い場合

  • コメント

    1. 匿名希望
      2017/06/28(水) 21:14:49

      サービスイン前だからデータが入ってないカラムに対してひたすらクエリを書く 実行結果の確認作業は行えないので雰囲気で書いている

    記事に戻る

関連記事