1対多のテーブルをJOINし、駆動テーブルにLIMITを付ける

あけましておめでとうございます。
2022年も、オンラインコンサルタントをよろしくお願いいたします。

さて、弊社の開発の中で、1対多のテーブルをJOINし駆動テーブルの方でLIMITをしたいということがありました。かなり苦戦したので、ここに解決方法を記載しておきます。
イメージは下記です。

<イメージ>
とあるSNSのユーザーと、そのフォロワー情報を一覧で取ってくる。
【要件】
・ユーザーとフォロワーのid、名前、ログインID、パスワードを取得する
・取得するユーザー数(フォロワーの数は関係なし)の上限を100件とする

・usersテーブル

idnamelogin_idpassword
1
SNS太郎snstarou123456
2SNS次郎snsjirou123456
3SNS三郎snssaburou123456
4SNS四郎snssirou123456

・followersテーブル

followee_idfollower_id
12
13
24

LIMITなしで情報を取得しようとすると、下記のようなSQLになります。

SELECT
    followee.id, followee.name, followee.login_id, followee.password,
    follower.id as follower_id, follower.name as follower_name,
    follower.login_id as follower_login_id, follower.password as follower_password
FROM
    users AS followee
LEFT JOIN
    followers
ON
    followee.id = followers.followee_id
LEFT JOIN
    users AS follower
ON
    follower.id = followers.follower_id

この結果に対して、取得するfolloweeの数を制限したいのですが、今回のレコード数が「フォローフォロワー関係の数」となり、followeeのLIMITをすることができません。。。

今回は、GROUP_CONCATを使った方法で解決しました。
下記のSQLです。

SELECT
    followee.id, followee.name, followee.login_id, followee.password,
    GROUP_CONCAT(COALESCE(follower.id, 'NULL') ORDER BY follower.id) AS follower_ids,
    GROUP_CONCAT(COALESCE(follower.name, 'NULL') ORDER BY follower.id) AS follower_names,
    GROUP_CONCAT(COALESCE(follower.login_id, 'NULL') ORDER BY follower.id) AS follower_login_ids,
    GROUP_CONCAT(COALESCE(follower.password, 'NULL') ORDER BY follower.id) AS follower_passwords
FROM
    users AS followee
LEFT JOIN
    followers
ON
    followee.id = followers.followee_id
LEFT JOIN
    users AS follower
ON
    follower.id = followers.follower_id
GROUP BY followee.id
ORDER BY followee.id ASC
LIMIT 100

これにて、ユーザーとフォロワーの情報を取得し、LIMITを付けることもできました。

あとがき

今回、SNSの例を使ったんですが、実際にはフォロワーの数が膨大になるので今回の解決方法は現実的でないですね。。。
1つのデータに対するJOIN数が少ない場合は、今回の方法が役に立つかと思います。
もっと良い方法があるという場合は、コメントいただけると嬉しいです。

プロパティにオブジェクトを持つオブジェクトをクローンする

つい最近の開発でハマったポイントだったので、こちらに記載しておきます。

とある変数にオブジェクトを格納した後、その変数を他の変数に代入したい。そんなときがあると思います。
しかしただ代入するだけだと、下記のような挙動をし、思っていたのとは違う結果を生みます。

<?php
class classA {
    public $name = 'classA';
    public $info = '1つ目のクラス';
}

$a = new classA();
$b = $a;
$b->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . PHP_EOL; // classA 1つ目のクラス 変更をしました
echo $b->name . ' ' . $b->info . PHP_EOL; // classA 1つ目のクラス 変更をしました

(かなり細かい話になるので、ここは飛ばしても良いかもしれません。)
この現象の理由は、$aに入っているのは「オブジェクト」ではなく、「オブジェクトが入っているメモリへのアドレス」だからです。
$bにメモリへのアドレスが代入され、$b->infoの変更は、アドレス先にあるオブジェクトを変更するという意味になります。
$aと$bが指しているオブジェクトは同じものになるので、$bの変更が$aにも反映されているのです。

これを防ぐには、下記の「clone」を用います。

<?php
class classA {
    public $name = 'classA';
    public $info = '1つ目のクラス';
}

$a = new classA();
$b = clone $a; // ココがポイント!!!
$b->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . PHP_EOL; // classA 1つ目のクラス
echo $b->name . ' ' . $b->info . PHP_EOL; // classA 1つ目のクラス 変更をしました

さて、ここから今回の本題になります。
下記のコードは、先ほどのクラスにオブジェクトのプロパティを追加したものです。

<?php
class ParentClass {
    public $name = 'ParentClass';
    public $info = '親クラス';
    public ChildClass $child;

    public function __construct()
    {
        $this->child = new ChildClass();
    }
}

class ChildClass {
    public $name = 'ChildClass';
    public $info = '子クラス';
}

$a = new ParentClass();
$b = clone $a;
$b->info .= ' 変更をしました';
$b->child->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . ' ' . $a->child->name . ' ' . $a->child->info . PHP_EOL;
// ParentClass 親クラス ChildClass 子クラス 変更をしました
echo $b->name . ' ' . $b->info . ' ' . $b->child->name . ' ' . $b->child->info . PHP_EOL;
// ParentClass 親クラス 変更をしました ChildClass 子クラス 変更をしました

clone後、$bのParentClassとChildClassに変更を加えています。
すると、なんとParentClassの変更は$aに反映していないのに、ChildClassの変更は$bに反映されています。

ちなみに、var_dumpの結果は以下のようになります。(上:$a、下:$b)
ParentClassは$aと$bで違っていますが、中身のChildClassは同じになっています。

object(~~Class)の後の、[]に注目!

ChildClassもcloneしたい場合、下記のようにします。

<?php
class ParentClass {
    public $name = 'ParentClass';
    public $info = '親クラス';
    public ChildClass $child;

    public function __construct()
    {
        $this->child = new ChildClass();
    }

    // ココがポイント!!!
    public function __clone()
    {
        $this->child = clone $this->child;
    }
}

class ChildClass {
    public $name = 'ChildClass';
    public $info = '子クラス';
}

$a = new ParentClass();
$b = clone $a;
$b->info .= ' 変更をしました';
$b->child->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . ' ' . $a->child->name . ' ' . $a->child->info . PHP_EOL;
// ParentClass 親クラス ChildClass 子クラス
echo $b->name . ' ' . $b->info . ' ' . $b->child->name . ' ' . $b->child->info . PHP_EOL;
// ParentClass 親クラス 変更をしました ChildClass 子クラス 変更をしました

ParentClassに__clone()というメソッドを追加します。これにより、ParentClassのインスタンスがcloneされるときに、$childもクローンしてねという指示を出すことができます。

var_dumpの結果も、以下の通りです。

FullCalendarを使う

最近、弊社製品ODINに機能アップデートがあり、配送予定をカレンダーで管理できるという機能が追加されました。
ここで使われている FullCalendar について、どのようなことができて、どのように使うのか紹介します。

できるもの

カレンダー上に予定を表示します。
日付欄や予定がクリックされたときには、FullCalendar独自のイベントが発生し、その予定に関連した処理をさせることができます。

カレンダーの表示

<div id=”calendar”>にカレンダーを描画するという処理を行っています。

<div id="calendar"></div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.5.0/main.min.css">
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.5.0/main.min.js"></script>
<script type="text/javascript">

    document.addEventListener('DOMContentLoaded', function () {

        let calendarEl = document.getElementById('calendar');
        let calendar = new FullCalendar.Calendar(calendarEl, {
            initialView: 'dayGridMonth'
        });

        calendar.render();

    });

</script>

FullCalendarでは予定の表示方法が複数提供されていて、1つ1つの表示方法を「view」としています。
initialViewに「dayDridMonth」というviewを指定することで下のような見た目になります。

予定の表示とコールバック関数の設定

let calendar = new FullCalendar.Calendar(calendarEl, {

        initialView: 'dayGridMonth',

        events: [
           {
                title: '予定1',
                start: '2021-09-15 10:00:00',
                end: '2021-09-15 12:00:00',
           },
           {
               title: '予定2',
               start: '2021-09-17 21:00:00',
               end: '2021-09-18 02:00:00'
           },
           {
               title: '予定3',
               start: '2021-09-20',
               allDay: true
           }
       ],

       dateClick: function (jsEvent) {
           alert('日付がクリックされました。\n' + jsEvent.dateStr);
       },

       eventClick: function (jsEvent) {
           alert(jsEvent.event.title+'がクリックされました。\n' + jsEvent.event.startStr);
       }
});
日付のクリック
予定のクリック

このクリックイベントを使えば、ユーザーに予定を登録・編集させることができます。

また、今回は「events」オプションに直接予定を記入していますが、JSONを返すスクリプトのURLを指定することでも、予定を設定できます。

document.addEventListener('DOMContentLoaded', function () {

   let calendarEl = document.getElementById('calendar')
   let calendar = new FullCalendar.Calendar(calendarEl, {

       initialView: 'dayGridMonth',

       events: '/getEvents.php',

       dateClick: function (jsEvent) {
            alert('日付がクリックされました。\n' + jsEvent.dateStr);
       },

       eventClick: function (jsEvent) {
           alert(jsEvent.event.title+'がクリックされました。\n' + jsEvent.event.startStr);
       }

});

PHPStormでUML図を書く

1. 経緯

現在、取り組んでいる開発でUML図書きたいなと思いました。
開発環境を、できるだけPHPStormで完結させるようにしていたので、どうせならUML書くのもPHPStormでやろうと調べました。

2. PlantUML

UMLを書くことに特化した言語です。
書いたコードを読み取り、UML図をレンダリングします。
参考:https://plantuml.com/ja/

3. 導入方法

① PHPStormにPlantUMLのプラグインをインストールする。

PHPStormの File > Settings > Plugins から「PlantUML integration」というプラグインをインストール&PHPStormの再起動を行います。

② Graphvizをインストール

シーケンス図(クラス間のやり取りを時系列で表した図)以外をレンダリングしたい場合、必要になるそうです。こちら からダウンロードできます。

③ PlantUMLファイル作成

他のファイルを追加するときと同じ要領で作成できます。
New > PlantUML File を選択し、ファイル名を入力します。
ここで、作りたい図がどれなのか選択すると、それに応じたテンプレートが自動で作成されます。

4. まとめ

今回PHPStormでPlantUMLを書き、UML図を作成するところまで行いました。コードベースで作成ということでデザインも崩れにくいですし、バージョン管理もしやすいのが良いですね。

新人プログラマーが「リーダブルコード」を読んでみた(後編)

こんにちは。
以前、新人プログラマーが「リーダブルコード」を読んでみた(前編)を投稿した者です。今回は、その続きを書きます。
今回も、各セクションごとに重要なポイントの記載に感想を交えていきます。

9. 変数と読みやすさ

<重要ポイント>

  • 不要な変数を削除し、変数の数を減らす。
  • 変数のスコープをできるだけ縮める。
  • 変数が更新される回数をできるだけ減らす。

コードを書いている時って、機能のことばかり考えていて、変数はそれを実現するための手段と考えてしまいがちだと思います。すると、変数をどう扱おうかって割と抜けちゃうんですよね…。
この本を読み、コードを書いているときの変数について気にすることをマニュアル化してくれると、今後意識できそうな気がします。

10. 無関係下位問題を抽出する

<重要ポイント>

  • プロジェクト固有のコードから、汎用的な機能を取り出し、汎用コード化する。
  • 1つのコードブロックの、高レベル目標を明確にし、それ以外に気を回す必要がないコードにする。
  • 小さな関数を作りすぎると、逆に読みにくくなるのでやり過ぎないようにする。

これは結構、意識していることではあります。
というのも、複雑なことをいくつも考えることができないので、自分の中で整理しておかないと、コードが把握しきれません。(家も散らかってくるとストレス感じます。。。)
ただ、結構行き当たりばったりな整頓をしている気がするので、ちゃんと計画を立ててコードを書かないといけないですね。

11. 一度に1つのことを

<重要ポイント>

  • 「読みにくい」コードがあった時に、そこで行われているタスクを列挙する。
  • タスクを別関数化、クラス化する。
  • それ以外の部分は、関数の論理的な段落となる。

10章で、コードを整理しなくてはという話がありました。
そこへの具体的なアプローチがこの章で記述されています。
「このごちゃごちゃコード、どこから手つけます…?」→「とりあえず列挙するか…」というムーブができるのは個人的にすごく大きいと思いました。

12. コードに思いを込める

<重要ポイント>

  • コードで行っていることや、ロジックを言葉にして説明してみる。
  • そのコンテクストに沿った形でコードを書いてみる。
  • ライブラリが提供してくれるものを知ることで、コードがより簡潔となる可能性がある。

チーム開発で大事なのは、一緒に開発する仲間との意思の疎通です。人間同士が意思の疎通を図るのに、一番良いのは会話だと思います。
分かりやすいコードについてのテクニックが、ここまでの章で紹介されていましたが、結局、文脈や実際の言葉に則ったコードが、分かりやすいということを伝えてくれています。
また、これは仲間に対してだけでなく自分に対しても有用です。
言葉にすることで、思考が整理されるんですね。
分かりにくいコードを、どうにも改善できないというときに、これを試してみたいと思います。

13. 短いコードを書く。

<重要ポイント>

  • 使っていないコードを削除する。
  • 過剰な機能を持たせない。
  • 標準ライブラリに慣れ親しむ。

使っていないコードを削除するというのは、当然のこととして、過剰な機能を持たせない。というのは、非常に心に残りました。
サービスを作るときには、ユーザーの視点になって考えることが大事ですが、そうすると、あれもこれもと機能を付けてしまいがちです。
何か機能を加えるときは、それが本当に必要なのか、別の何かで代替できないかを考え、判断しようと思います。

14. テストと読みやすさ

<重要ポイント>

  • 入出力のテストを1行で記述する。
  • 分かりやすいエラーメッセージを表示するようにする。(カスタマイズも可)
  • テストが、本物コードの読みやすさを阻害したり、開発の邪魔にならないよう、やり過ぎないようにする。
  • テスト駆動開発については様々な議論があるが、テストがしやすいようにコードをかくことは有効。

テストを分かりやすく書く方法についての章です。
テストの充実は、プロジェクトの保守性に非常に効果的ですし、テストこそ、他の人がよく編集する場所だと思います。なので、重要性は言わずもがなですが、同時に「テストがしやすいようにコードを書く」ことの大事さがここに書いてあります。
これって、テストについて言っているようで、これまでの内容のおさらいにもなっているんですよね。いかに機能を取り出して関数化、クラス化するか。
関数化もやりすぎは良くない。という話がありましたが、この「テストしやすいように」というのは、1つの物差しとなるのではないかと思います。

まとめ

前半に引き続き、後半についても内容をまとめさせていただきました。
非常に読みやすく、ページ数も多くない本でしたが、内容はかなり濃厚でした。
新人プログラマーや、コードがうまく書けないという人には是非おすすめしたいです。


正直なところ、この本に書いてあったことが今すぐできるかと問われれば、無理だと思います。
なので、この記事と本を何度も読み返すつもりです。