Friction River Software

  • お問い合わせ

CakePHP5入門【CakePHP5基礎編⑥】データベース操作②

B美

今回も「データベース操作」の続きです

A子

おー!

C菜

やるです~

B美

まずはSQLの中のSELECTセレクト文(選択せんたく射影しゃえい結合けつごうという関係演算を行う命令)なんだけど…

SELECT 項目名列挙 | *
FROM 表名列挙
[WHERE 条件1]
[GROUP BY 項目名列挙]
  [HAVING 条件2]
[ORDER BY 項目名列挙]

というのが基本形ね
(「| *」は「または*」という意味)

C菜

列挙れっきょ」って、どういう意味ですか~?

B美

カンマ区切りで複数書ける…って意味よ
(もちろん一つだけでもOK)

A子

[ ]で囲んでいるのって何よ?

B美

省略可能って意味よ

あ、「HAVINGハビング」句だけは単独で使えないからね
(必ず「GROUP BYグループ・バイ」句と併用すること)

A子

そのHAVINGってやつでも使ってるけど、「条件1」と「条件2」の違いって何?

B美

GROUP BY」句って、グループ化を行う命令なんだけどさ

・「WHERE」句 … グループ化を行う「」の条件指定
・「HAVING」句 … グループ化を行った「」の条件指定

…ってわけ

C菜

あんまりイメージできないです~

B美

具体例を書けば、すぐに分かると思うわよ(多分…)

まずは…

SELECT id,remarks,counter
FROM counters
WHERE id >= 5;

を実行してみてね

C菜

MATE端末」で

mysql -u root -p testdb[Enter]

を実行してからですよね~?

B美

もちろん!



A子

もともとのデータって、下に示す値だったよね?
ふむ、なるほど~

SELECT」のあとに項目名を列挙すると、その列の値だけが表示されるのね
あと「WHERE」の条件として「id」が5以上って指定してるから、1~4の「id」のレコードが消えてるのか…



B美

その通り!
必要な列(項目フィールド)のみ抽出することを「射影プロジェクション」、条件に合った行(レコード)のみ抽出することを「選択セレクション」と呼びます

ちなみに、一行で打ち込んでも良いし、途中で改行してもOKよ
(SQLでは;セミコロン」を入力して[Enter]キーを押さない限り、命令は実行されないから…)

あと、分かりやすいように「」を大文字で記述してるけど、(前にも言った通り)SQLでは大文字と小文字を区別しないからね
(普通は全て「小文字」で打ち込みます…面倒くさいので(笑))

それじゃ、次は「GROUP BY」句を使ってみましょう

SELECT remarks,sum(counter)
FROM counters
WHERE id >= 5
GROUP BY remarks;

C菜

「id」が5以上のレコードの「test01」の値は30と40と50なので、その合計が120ということですね~

つまり、sum(counter)は「counter」という項目の値を合計する関数だと思うです~

B美

大正解!
それらを集合関数と呼んでるんだけど、ほかにも「avgアベレージ()」「minミン()」「maxマックス()」「countカウント()」なんかがあるわよ

・avg() … 平均値
・min() … 最小値
・max() … 最大値
・count() … レコード数

ちなみに、countだけはちょっと特殊なやつで、検索結果のレコード数(件数)を取得するの
(なので、カッコ内には項目名を書かずに「count(*)」という形で用いることが多いわね)

A子

例えば、「その合計したものが100以下のやつだけ表示したい」って場合、どうするの?

B美

ふふ
それこそがさっき言ったグループ化「」の条件指定なのよ
だって、「remarks」の値でグループ化しないと、「sum(counter)」は計算できないでしょ?

んじゃ、さっきのSQL文に「HAVING」句を追加してみてね

SELECT remarks,sum(counter)
FROM counters
WHERE id >= 5
GROUP BY remarks
HAVING sum(counter) <= 100;

C菜

「test02」と「test03」だけが表示されました~

あ、もしも「WHERE」句を削除しちゃったらどうなるんでしょうか~?

A子

全てのレコード(14レコード)が対象になるから、「top」が追加で表示されたってわけね

うーん、「WHERE」条件と「HAVING」条件の違いが(なんとなく)分かったよ

B美

最後は「ORDER BY」句ね

SELECT id,remarks,counter
FROM counters
ORDER BY counter DESC;

を実行してみてね

C菜

「counter」という項目の値が降順ソートされてます~

これって「DESC」を付けてるからでしょうか~?

B美

そうよ

末尾に「ASC」を付けるか、何も付けなければ「昇順しょうじゅん」ソート
「DESC」を付ければ「降順こうじゅん」ソートになるのよ

C菜

あ、もしかして英単語の「ascendingアセンディング orderオーダー」と「descendingディセンディング orderオーダー」から来てるんじゃないでしょうか~?

A子

んん?
何それ…?

B美

さすがはC菜ね
その通りよ

ascending order」が「昇順」
descending order」が「降順」

…って意味ね

だから「ASCアスク」と「DESCデスク」なのよ

A子

あれ?
なんだか今回ってSQLばっかりやってるけど、「CakePHP」はどうなったのよ

B美

いや、「急がば回れ」ってやつだから…(苦笑)

実はModelモデルクラスのメソッド名が「select()」とか「where()」なのよね

C菜

そういえば以前使ったコードの中にも「where」メソッドがありましたね~

$counters = $this->Counters->find()->where(['id' => 1])->first();

…って感じで~
(「CakePHP5入門【CakePHP5基礎編③】MVC」を参照)

A子

あー、あったねー

そっか、これって「idが1と等しいレコードを抽出」って意味かー

B美

そういうこと

ちなみに、検索結果って「連想配列」が「配列」化されたもの(複数行あるから)になるんだけど、結果が1レコードしかないのが明らかな場合って、末尾に「->first()」を付けることで「配列」化しないようにもできるの

A子

あ、だからViewビューファイルのほうでforeachを使わなかったのか…

直接「$count['remarks']」や「$count['counter']」って書いてたもんね

C菜

ですです~

言い換えれば、「first」メソッドを呼ばない場合、必ず「foreach」で処理するって考えておけば良いですかぁ~?

B美

うーん、惜しい…
検索結果の全レコードを取得するには「all」メソッドを呼ぶ必要があるのよ
(だから「firstメソッドを呼ばない場合」ではなく、「allメソッドを呼んだ場合」という表現が正確かな)

ちなみに、前回(CakePHP5入門【CakePHP5基礎編⑤】データベース操作①)、Viewビューファイルである「testapp/templates/Counters/index.php」の中身を見てなかったけど、実はこのように「foreach」を使ってたの

A子

ちょっと待って…

foreachの書き方が以前に習ったのと違うんだけど?

B美

実は

foreach ($counters as $counter) {

}

って

foreach ($counters as $counter):

endforeach;

という風に書くこともできるの

どっちを使っても構わないんだけど、私は上の書き方のほうが好みね
(だからPHPの文法編では、そっちを説明したってわけ)

C菜

あ、もしかして~

if」文や「while」文も同じですか~?

B美

正解
波カッコを使わず、閉じるのに「endif」や「endwhile」を使う方法もあるわ

ただまぁ、コードを打ち込む量が増えるから、私は嫌いだけどね

A子

確かに…(笑)

C菜

さっきの「index.php」なんですけど、「$counter['id']」じゃなくて「$counter->id」という書き方になってますよ~

これって連想配列じゃなかったんですかぁ~?

B美

お、そこに気が付くとはなかなか…

さっき「データベースの検索結果は連想配列の配列」って言ったけど、実は嘘っぱちだから…(苦笑)

A子

はぁ~?

いったいどういうことよ(怒)

C菜

あ、わかりました~

->アロー」を使ってるってことは、これってきっと「オブジェクト」のはずです~

B美

さっすがC菜!
実はデータベースの検索結果って、「ResultSet」クラスのオブジェクトなのよ

ただ、さっきも言ったように「連想配列が配列化されたもの」のごとく取り扱えるってわけ
まぁ、項目フィールドへのアクセスは->アローでやったほうが文字数が少なくなるから良いんだけど…(苦笑)

A子

なるほどねぇ

まぁ確かに「$counter['id']」よりは「$counter->id」のほうがシンプルで分かりやすいかも…
(キーボードを打つ量も減るし…)

B美

それではようやく今回の本題です

testapp/src/Controller/CountersController.php」を開いて、次のように「search」メソッドを追記してね

public function search($id = 1)
{
  $query = $this->Counters->find();
  switch ($id) {
    case 1:$result = $query->all();
       break;
    case 2:$result = $query->select(['id', 'remarks', 'counter'])->where(['id >=' => 5])->all();
       break;
    case 3:$result = $query->select(['remarks', 'sum_counter' => $query->func()->sum('counter')])->where(['id >=' => 5])->group('remarks')->all();
       break;
    case 4:$result = $query->select(['remarks', 'sum_counter' => $query->func()->sum('counter')])->where(['id >=' => 5])->group('remarks')->having(['sum_counter <=' => 100])->all();
       break;
    case 5:$result = $query->select(['remarks', 'sum_counter' => $query->func()->sum('counter')])->group('remarks')->having(['sum_counter <=' => 100])->all();
       break;
    case 6:$result = $query->select(['id', 'remarks', 'counter'])->order(['counter' => 'desc'])->all();
       break;
  }
  $fields = array_keys($result->first()->toArray());
  $this->set(compact('fields', 'result'));
}

C菜

PHP言語の文法で、今まで習ってこなかったのが出てきましたよ~

switch」って何なんでしょうか~?

A子

うーん、なんとなくは推測できるけどねぇ

B美

多分、あなたたちの推測通りだと思うわよ

switch (変数) {
  case 値A:変数が左の値Aと等しいときの処理;break;
  case 値B:変数が左の値Bと等しいときの処理;break;
  case 値C:変数が左の値Cと等しいときの処理;break;
  case 値D:変数が左の値Dと等しいときの処理;break;
  case 値E:変数が左の値Eと等しいときの処理;break;
  case 値F:変数が左の値Fと等しいときの処理;break;
}

…ってことよ

A子

やっぱり多分岐の処理か

if elseif elseif …ってことでしょ?

B美

正解!
分岐の数が多いときは「switch」文のほうが見やすいからね

あ、一つ注意点があります

それぞれのcaseの最後には必ず「break;」を記述すること
でないと、「break;」が出現するまで「switch」ブロック内を全て実行しちゃうことになるからね

C菜

「search」メソッドの引数にある「$id = 1」って何ですか~?

B美

ブラウザのURL上で「search」メソッドにパラメータを渡す仕組み

「localhost/testapp/counters/search/1」のように、URLの末尾に「/数値」を付けるの
そうすると、「search」メソッドの引数「$id」に数値(上記の例ならば1)が渡るってわけ

ちなみに「= 1」ってのはデフォルト値で、「localhost/testapp/counters/search」のようにアクセスした場合は「$id」が「1」になります

A子

そういえば、「view」や「edit」のメソッドでもやってたね
(デフォルト値は「null」だったけど…)

B美

んじゃ次ね

testapp/templates/Counters」ディレクトリ内に「search.php」というファイルを作ります

<table>
  <?php foreach ($fields as $field) { ?>
    <th><?= h($field) ?></th>
  <?php } ?>
  <?php foreach ($result as $value) { ?>
    <tr>
      <?php foreach ($fields as $field) { ?>
        <td style="line-height: 10px;"><?= h($value->get($field)) ?></td>
      <?php } ?>
    </tr>
  <?php } ?>
</table>


A子

これまでちょっと聞きそびれていたんだけど、「h()」って何?

B美

以前「htmlspecialcharsエイチティーエムエルスペシャルキャラクターズ」という関数を使ったの、憶えてる?

実は「h()」って「htmlspecialchars()」と同じものなのよ

C菜

あ~
つまり、「サニタイジング」ってわけですね~

B美

そういうこと

それじゃ、実行確認していきましょう
まずはブラウザのURLに
localhost/testapp/counters/search/1
  または
localhost/testapp/counters/search
を入力してね

A子

これって
SELECT * FROM counters;

ってことかー

コード的には
$query = $this->Counters->find();
$result = $query->all();

…ってことになるね
(さっき打ち込んだ「search」メソッドの内容から抽出すると…)

B美

その通り
よく分かってるじゃん

では、次は
「localhost/testapp/counters/search/2

C菜

これは~
SELECT id,remarks,counter
FROM counters
WHERE id >= 5;

というSQLで~

コード的には
$query = $this->Counters->find();
$result = $query->select(['id', 'remarks', 'counter'])->where(['id >=' => 5])->all();

ですね~?

B美

正解
さぁ、どんどんいくわよー

次は
「localhost/testapp/counters/search/3

A子

「remarks」でグループ化して「counter」の合計を求めたやつだね
(ただし「id」は5以上で)

SQLは
SELECT remarks,sum(counter)
FROM counters
WHERE id >= 5
GROUP BY remarks;

で、コードは
$query = $this->Counters->find();
$result = $query->select(['remarks', 'sum_counter' => $query->func()->sum('counter')])->where(['id >=' => 5])->group('remarks')->all();

…ってことか

'sum_counter' => $query->func()->sum('counter')」の箇所だけが、少し(いや、かなり)ややこしい…(苦笑)

B美

私の個人的な意見だけど、集合関数はできるだけ使いたくないわね(笑)

んじゃ、次は
「localhost/testapp/counters/search/4

C菜

これは「HAVING」条件を指定したやつですね~

SQLは
SELECT remarks,sum(counter)
FROM counters
WHERE id >= 5
GROUP BY remarks
HAVING sum(counter) <= 100;

コードは
$query = $this->Counters->find();
$result = $query->select(['remarks', 'sum_counter' => $query->func()->sum('counter')])->where(['id >=' => 5])->group('remarks')->having(['sum_counter <=' => 100])->all();

です~

コードがすっごく長いです~(苦笑)

A子

だねー(笑)

B美

次は上記のコードから「where」条件を削除したやつよ

URLは
「localhost/testapp/counters/search/5

A子

「id」が5以上という条件がなくなったから、「top」というレコードが出現したやつだね

SQLは
SELECT remarks,sum(counter)
FROM counters
GROUP BY remarks
HAVING sum(counter) <= 100;

で、コードは
$query = $this->Counters->find();
$result = $query->select(['remarks', 'sum_counter' => $query->func()->sum('counter')])->group('remarks')->all();

…ってことかー

B美

最後は
「localhost/testapp/counters/search/6

C菜

これは「並べ替えソート」です~

SQLは
SELECT id,remarks,counter
FROM counters
ORDER BY counter DESC;

コードは
$query = $this->Counters->find();
$result = $query->select(['id', 'remarks', 'counter'])->order(['counter' => 'desc'])->all();

ですね~

B美

さて、ここまでで何か質問は?

SQL文とPHPコードを並べると、なんとなく見えてきたでしょ?

A子

まぁ、なんとなくは…ね(苦笑)

C菜

私はすごく良く分かりましたよ~

B美

あ、そうだ

余談だけど、「select」メソッドはあまり使わないわ
(集合関数を使いたいなら別だけど…)

A子

何で?

B美

全ての項目の値をViewビュー側に渡したとしても、それを使わなければ良いだけだもの
つまり「射影プロジェクション」は必須じゃないってこと

あと、グループ化もできるだけ使わないように(使わなくて済むように)データベース設計するかな?
(個人的な意見だけど…)

C菜

ということは~

where」メソッドと「order」メソッドの使い方さえマスターしておけば大丈夫…ってことでしょうか~?

B美

そういうことになるわね

そして、両方ともそれほど難しくはないでしょ?

A子

どっちも引数として渡すのは「連想配列」ってことだよね?

B美

その通り

ちなみに「where」メソッドに複数の条件を指定した場合、自動的にAND検索(~ かつ ~)になるからね

C菜

OR検索(~ または ~)したい場合はどうするんですか~

B美

「where」メソッドに渡す連想配列として、'OR' => [[条件1], [条件2]]って感じの要素を加えると…
「条件1または条件2」ってことになるわ
(条件1や条件2の個所も連想配列として記述)

例えば、「idが5以上」または「counterが10未満」という条件の場合
$query = $this->Counters->find();
$result = $query->where(['OR' => [['id >=' => 5], ['counter <' => 10]]])->all();

…って感じ

A子

ややこしい!

B美

心配しなくても、OR検索なんてめったに使わないわよ

さて、それでは今回はここまで
次回は直接SQL文を実行する方法を解説します