作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 9章テスト 9-1『ユニットテスト』

以下の記事は2019年1月10日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読会 (9章 テスト)の輪読会資料の一部となります。 今回は 9-1 章部分の『ユニットテスト』部分の記事をアップします。

また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 輪読会資料

Chapter 9 テスト

テストコード実装の基礎と実践

目次

  • 9-1 ユニットテスト
    • 9-1-1 テスト対象クラス
    • 9-1-2 テストクラスの生成
    • 9-1-3 テストメソッドの実装
    • 9-1-4 データプロバイダの活用
    • 9-1-5 例外のテスト
    • 9-1-6 テストの前処理・後処理
    • 9-1-7 テストの設定
  • 9-2 データベーステスト
    • 9-2-1 テスト対象のテーブルとクラス
    • 9-2-2 データベーステストの基礎
    • 9-2-3 Eloquentクラスのテスト
    • 9-2-4 サービスクラスのテスト
    • 9-2-5 モックによるテスト(サービスクラス)
  • 9-3 WebAPIテスト
    • 9-3-1 WebAPIテスト機能
    • 9-3-2 テスト対象のAPI
    • 9-3-3 APIテストの実装
    • 9-3-4 WebAPIテストに便利な機能


なお、説明中のサンプルコードは以下のリンクより取得したものです。
PHPフレームワーク Laravel Webアプリケーション開発 laravel-socym/chapter09 by GitHub

9-1 ユニットテスト

Lravelでサポートされるテスト機能


余談

いつも見るサンプルコードの冒頭の奴declare(strict_types=1);は、 厳密な型チェックモードに設定している。 これを記述することによって型指定をして、間違った値が入るとエラーを出すそうです。

9-1-1 テスト対象クラス

ここではポイント算出のメソッドを元にtestを行う
app\Services\CalculatePointService.php

<?php
declare(strict_types=1);

namespace App\Services;

use App\Exceptions\PreConditionException;

final class CalculatePointService
{
    /**
     * @param int $amount
     * @return int
     * @throws PreConditionException
     */
    public static function calcPoint(int $amount): int
    {
        if ($amount < 0) {
            throw new PreConditionException('購入金額が負の数');
        }

        if ($amount < 1000) {
            return 0;
        }

        if ($amount < 10000) {
            $basePoint = 1;
        } else {
            $basePoint = 2;
        }

        return intval($amount / 100) * $basePoint;
    }
}

要約すると以下の処理をするメソッド
買い物をした際に付く、〇〇ポイントの算出をする機能。以下のルールで行う

購入金額 ポイント
0~999 ポイント無し
1,000~9,999 100円につき1ポイント
10,000以上 100円につき2ポイント


9-1-2 テストクラスの生成

ユニットテストを記述するテストクラスは以下のコマンドで生成する

$ php artisan make:test CalculatePointServiceTest --unit
Test created successfully.

以下のパスにファイルが生成される
tests\Unit\CalculatePointServiceTest.php

9.1.2.4 生成されたCalculatePointServiceTestクラス

<?php

namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class CalculatePointServiceTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testExample()
    {
        $this->assertTrue(true);
    }
}

ここで大事な補足

書籍にはないが、 Testクラスを作成したらテスト対象となるクラスを use させないと駄目。 今回のテスト対象はApp\Services\CalculatePointServiceなので以下の一行をTestファイルのクラス宣言前に追加する。

<?php

namespace Tests\Unit;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
// この行を追加
use App\Services\CalculatePointService;

class CalculatePointServiceTest extends TestCase
{
    // 省略
}

リスト9.1.2.2 testsディレクトリの構成

tests
├─Feature                                // フィーチャ機能テストのディレクトリ
│  └─ExampleTest.php
├─Unit                                   // ユニットテストのディレクトリ
│  ├─CalculatePointServiceTest.php      // 生成されたテストクラス(これ以外はデフォルト)
│  └─ExampleTest.php
├──CreatesApplication.php
└──TestCase.php                         // テスト基底クラス

テストクラスを実装する際は Tests\TestCase クラスを継承することが多いが、フレームワークの機能を使わない場合は PHPUnit\Framework\TestCase クラスを直接敬称しても問題ない。

9.1.2.3 テストクラスのクラス図

f:id:sakamata:20190105192040p:plain

生成されたtestClassには以下の様な test... で始まるメソッドが作られる
test... から始まるメソッドがテストメソッドとして実行される。
9.1.2.4 抜粋 生成された CalculatePointServiceTest クラス

<?php
    public function testExample()
    {
        $this->assertTrue(true);
    }

別の方法でコメントに @test アノテーションを付ける方法もある。作者はこっちをお勧めしてる
参考: アノテーションについて
9.1.2.5 @testアノテーション

<?php
    /**
    * @test
    */
    public function Example()
    {
        $this->assertTrue(true);
    }

さらにアノテーションと、メソッド名を日本語にすると(え!?) test結果の判別が付けやすくなる

<?php
    /**
    * @test
    */
    public function divide_除数がゼロなら例外を投げる()
    {
        // (略)
    }

9.1.2.6 テストの実行例
phpunitコマンドに続けてテストクラスファイルを指定する

$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest.php
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 1.94 seconds, Memory: 10.00MB

OK (1 test, 1 assertion)

9.1.2.8 テストの失敗例

vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest.php
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 1.04 seconds, Memory: 10.00MB

There was 1 failure:

1) Tests\Unit\CalculatePointServiceTest::Example
Failed asserting that false is true.

/home/vagrant/larabook/chapter09/tests/Unit/CalculatePointServiceTest.php:19

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

コマンドの際 phpunit コマンドの引数を省略すると 全テストが実行される。よく使うので覚えておくと良い

9.1.2.9 全てのテストの実行例

$ ./vendor/bin/phpunit

但し、アプリケーションの規模が大きい場合は、テスト実行時間が恐ろしい事になるので安易やらない方が良い。(Ruby on Railsでの経験則)


9-1-3 テストメソッドの実装

ポイント付与のサンプルコードを元にテストメソッドを実装する
ここでは、ポイント算出ルールの境界値に沿ってテストを記述する
まず、購入金額が0円のパターンを検査する
0円の場合ポイントが0になるか?

9.1.3.2 主なアサーションメソッド(抜粋)

メソッド 内容
assertSame 型も含めて期待値と値が一致するかを検証
assertTrue 値がtrueかどうかを検証
assertReqExp 値が正規表現にマッチするかどうかを検証
assertArrayHasKey 値が配列の場合、指定したキーが存在するかを検証

tests\Unit\CalculatePointServiceTest.phpに以下のメソッドを追加

<?php
    /**
     * @test
     */
    public function calcPoint_購入金額が0ならポイントは0()
    {
        $result = CalculatePointService::calcPoint(0);
        $this->assertSame(0, $result); // $result が0である事を検証
    }

テストを実行すると2つのtestが通る事を確認できる

vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 4.43 seconds, Memory: 10.00MB

OK (2 tests, 2 assertions)

同様に今度は購入金額1000円でのテストを書く

<?php
    /**
     * @test
     */
    public function calcPoint_購入金額が1000ならポイントは10()
    {
        $result = CalculatePointService::calcPoint(1000);
        $this->assertSame(10, $result); // $result が10である事を検証
    }

同様にテストが3つ通る事を検証

vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 1.03 seconds, Memory: 10.00MB

OK (3 tests, 3 assertions)

こんな感じでテスト項目を増やすって事らしい


9-1-4 データプロバイダの活用

テストメソッドは引数と戻り値の組み合わせのみだが、データプロバイダは同じ処理に対して、異なるパラメータや引数を渡してテストする事ができて便利。
テストメソッドに渡すパラメータを指定するメソッドを用意する
データプロバイダメソッドは public にする必要がある

9.1.4.1 データプロバイダメソッドの例

<?php
    public function dataProvider_for_calcPoint(): array
    {
        return [
            '購入金額が0なら0ポイント'       => [0, 0],
            '購入金額が999なら0ポイント'     => [0, 999],
            '購入金額が1000なら10ポイント'   => [10, 1000],
        ];
    }

データプロバイダを利用するには、テストメソッドに @dataProviderアノテーションを指定する。

9.1.4.2 データプロバイダを利用したテストメソッド

<?php
    /**
     * @test
     * @dataProvider dataProvider_for_calcPoint
     */
    public function calcPoint(int $expected, int $amount)
    {
        $result = CalculatePointService::calcPoint($amount);
        $this->assertSame($expected, $result); 
    }

仮に以下の値をあえて間違った上でテストを実行してみる '購入金額が1000なら10ポイント' => [0, 1000],

vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/CalculatePointServiceTest
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

.....F                                                              6 / 6 (100%)

Time: 1.34 seconds, Memory: 10.00MB

There was 1 failure:

1) Tests\Unit\CalculatePointServiceTest::calcPoint with data set "購入金額が1000なら10ポイント" (0, 1000)
Failed asserting that 10 is identical to 0.

/home/vagrant/larabook/chapter09/tests/Unit/CalculatePointServiceTest.php:59

FAILURES!
Tests: 6, Assertions: 6, Failures: 1.

1エラーとなりエラーとなった配列のキー(日本語)と、配列の組み合わせ(0,1000)が出力されるので、エラー詳細が明快になる。

より多くのテストパターンを実装、数値の境界部分の値を追加している
9.1.4.6 データプロバイダメソッドに要素を追加

<?php
    public function dataProvider_for_calcPoint() : array
    {
        return [
            '購入金額が0なら0ポイント' => [0, 0],
            '購入金額が999なら0ポイント' => [0, 999],
            '購入金額が1000なら10ポイント' => [10, 1000],
            '購入金額が9999なら99ポイント' => [99, 9999],
            '購入金額が10000なら200ポイント' => [200, 10000],
        ];
    }

こんな感じでデータプロバイダメソッドでは複数の値、パラメータでテストが行える。


9-1-5 例外のテスト

throw で投げられた例外処理をテストするには、以下の利用方法がある

作者が薦めるのは3つめのアノテーション、ここでは

  • 例外がスローされるか?
  • スローされた例外が意図したものであるか?

を検証する

言葉の意味は expect Exception (期待される 例外)となる

以下3つの例はこのコードのみで完結する自己循環型のテスト例で、CalculatePointServiceをテストしている訳では無い

try/catchの利用

通常のPHPコードと同様にテスト対象を tryで囲む奴を使う

<?php
    /**
     * @test
     */
    public function exception_try_catch()
    {
        try {
            throw new \InvalidArgumentException('message', 200);
            $this->fail(); // (1)例外がスローされない時はテストを失敗させる
        } catch (\Throwable $e) {
            // 指定した例外クラスがスローされているか
            $this->assertInstanceOf(\InvalidArgumentException::class, $e);
            // スローされた例外のコードを検証
            $this->assertSame(200, $e->getCode());
            // スローされた例外のメッセージを検証
            $this->assertSame('message', $e->getMessage());
        }
    }

InvalidArgumentException 引数の型が期待する型と一致しなかった場合にスローされる例外。

expectException メソッドの利用

<?php
    /**
     * @test
     */
    public function exception_expectedException_method()
    {
        // 指定した例外クラスがスローされているか
        $this->expectException(\InvalidArgumentException::class);
        // スローされた例外のコードを検証
        $this->expectExceptionCode(200);
        // スローされた例外のメッセージを検証
        $this->expectExceptionMessage('message');

        throw new \InvalidArgumentException('message', 200);
    }

@expectedExcepsion アノテーションの利用

作者押しの例外テスト方法
アノテーション(コメント)部分に必要事項を書いてしまう

<?php
    /**
     * @test
     * @expectedException \InvalidArgumentException
     * @expectedExceptionCode 200
     * @expectedExceptionMessage message
     */
    public function exception_expectedException_annotation()
    {
        throw new \InvalidArgumentException('message', 200);
    }

9.1.5.4 購入金額が負数の場合のテスト

テスト例となる CalculatePointService classのメソッドに以下の様な throw 処理がある、これをテストする

<?php
        if ($amount < 0) {
            throw new PreConditionException('購入金額が負の数');
        }

ちなみにPreConditionExceptionだが、ファイルを見ても特に何も書いていないが、ちゃんとしたアプリケーションならここにちゃんと例外を起こした際に書くべき処理を書けって事かな?ここでのテストはちゃんとそこに正しい値が投げられるようになっているかを検証するまでを行う。

app\Exceptions\PreConditionException.php

<?php
declare(strict_types=1);

namespace App\Exceptions;

final class PreConditionException extends \Exception
{

}

例外テストの具体例

<?php
    /**
     * @test
     * @expectedException \App\Exceptions\PreConditionException
     * @expectedExceptionMessage 購入金額が負の数
     */
    public function calcPoint_購入金額が負の数なら例外をスロー()
    {
        CalculatePointService::calcPoint(-1);
    }

マイナスの値がcalcPointに投げられた際を検証するために アノテーションで ’@expectedException'で使用される例外処理先\App\Exceptions\PreConditionExceptionを指定する

つまり『この異常な値を放り込んだら、この Exceptionにこのメッセージが飛んでる?』って事をテストしてる。当然だが、メッセージをちょっとでも変えるとテストは通らずエラーになる。


9-1-6 テストの前処理・後処理

テスト前にDBに必要なの値の仕込みや、テスト後に値を削除変更する必要がある場合、テストメソッドに書くと煩雑になるので、別途専用にメソッドが用意されている。PHPUnit\Framework\TestClassにあるテンプレートメソッド

  • 前処理 setUp メソッド
  • テスト中 testメソッド
  • 後処理 tearDown メソッド

の順で呼ばれて処理される

また、テストクラス毎に呼ばれる以下もある

  • setUpBeforeClass メソッド
  • tearDownAfterClass メソッド

これらはテストメソッドが属するテストクラス毎に1回だけ呼ばれる

9.1.6.1 テンプレートメソッドの動きを見るテスト
どんな順番で処理が実行されるかを確かめるメソッド群

<?php
declare(strict_types=1);

namespace Tests\Unit;

use App\Services\CalculatePointService;
use Tests\TestCase;

class TemplateMethodTest extends TestCase
{
    public static function setUpBeforeClass()
    {
        parent::setUpBeforeClass();

        echo __METHOD__, PHP_EOL;
    }

    protected function setUp()
    {
        parent::setUp();

        echo __METHOD__, PHP_EOL;
    }

    /**
     * @test
     */
    public function テストメソッド1()
    {
        echo __METHOD__, PHP_EOL;
        $this->assertTrue(true);
    }

    /**
     * @test
     */
    public function テストメソッド2()
    {
        echo __METHOD__, PHP_EOL;
        $this->assertTrue(true);
    }

    protected function tearDown()
    {
        parent::tearDown();

        echo __METHOD__, PHP_EOL;
    }

    public static function tearDownAfterClass()
    {
        parent::tearDownAfterClass();

        echo __METHOD__, PHP_EOL;
    }
}

上記のテストを実行すると以下の様な順序でテストが行われる事が確認できる

vagrant@homestead:~/larabook/chapter09$ ./vendor/bin/phpunit tests/Unit/TemplateMethodTest.php
PHPUnit 6.5.9 by Sebastian Bergmann and contributors.

Tests\Unit\TemplateMethodTest::setUpBeforeClass
.Tests\Unit\TemplateMethodTest::setUp
Tests\Unit\TemplateMethodTest::テストメソッド1
Tests\Unit\TemplateMethodTest::tearDown
.                                                                  2 / 2 (100%)Tests\Unit\TemplateMethodTest::setUp
Tests\Unit\TemplateMethodTest::テストメソッド2
Tests\Unit\TemplateMethodTest::tearDown
Tests\Unit\TemplateMethodTest::tearDownAfterClass


Time: 1.1 seconds, Memory: 10.00MB

OK (2 tests, 2 assertions)

これらのテンプレートメソッドを使う際はparentで継承元メソッドを呼ぶことを忘れないようにすること
parent::setUp()


9-1-7 テストの設定

Laravelのルートディレクトリにあうphpunit.xmlファイルで以下の設定項目の編集が可能

  • PHPUnitに関する設定の変更が可能
  • また、テストディレクトリやテスト対象ファイルの追加や変更を設定可能
  • テスト実行時にPHPの設定を変更する
  • 環境変数によってアプリケーション設定を変更する

詳細な設定に関しては PHPUnit の公式マニュアルを参照のこと


【輪読会資料】達人に学ぶSQL徹底指南書 第2版  9, SQLで集合演算

以下の記事は2018/12/26 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 達人に学ぶSQL徹底指南書 輪読会 第1部 魔法のSQL (集合演算/数列) の資料となります。
以下の書籍の 第一部 9 SQLで集合演算の要約です。

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)



9 SQLで集合演算

見出し一覧

  • SQL集合論
  • はじめに
  • 導入---集合演算に関するいくつかの注意点
  • テーブル同士のコンベア---集合の相等性チェック[基本編]
    • [Extra おさらい] UNION の簡単な使用例
  • テーブル同士のコンベア---集合の相等性チェック[応用編]
  • 差集合で関係除算を表現する
  • 等しい部分集合を見つける
  • 重複行を削除する高速なクエリ
  • まとめ
  • 演習問題

1版の記事、及びサンプルコードは以下リンク先よりDL可能
達人に学ぶSQL SQLで集合演算 集合指向言語としてのSQL:その4

サンプルコード
2版用に独自アレンジしたもの
Dropbox - 9_SQLで集合演算_サンプルコード.txt

SQLを手軽にシミュレートするにはこちらのサイト

sqlfiddle(但しdelete系は使用不可、またちょいちょい調子悪くなる)
http://sqlfiddle.com/

ORACLE Live SQL(アカウント登録が必要)
Oracle Live SQL



SQL集合論

近年までSQLは集合演算の整備をしていなかった為、その機能は十分活用されてこなかった。
が、最近になって機能が出揃ってきた。
ここは集合演算の背景にある考え方を理解する章となる。

[Extra] そもそも集合論とは?

高校位の数学でやった筈なので、おおよそのエンジニアさんには釈迦に説法かもですが、俺が再認識したいので以下にまとめます。

部分集合 included, subset

f:id:sakamata:20181224151819p:plain:w300
『 A は B の部分集合である 』といい

A ⊆ B
と表現される

また、日本の高等数学では
A ⊂ B
を使用する

俺要約 要は 不等号の ≦ でのイメージをそのまま転写して『含む』で思考すれば良い

A は B に包まれる(included; 包摂あるいは内包される)などともいう



和集合 union

和 合併集合 合併(union)  などともいう
f:id:sakamata:20181224151854p:plain:w300
『すくなくとも片方に入っているもの』を集めた集合

A ∪ B

と表現される 俺要約 要は OR関数(または)で定義する範囲の結果を示す 記号の覚え方は 広く囲む掌をイメージ



積集合 intersection

『両方ともにはいっているもの』を集めた集合
共通部分 ともいう
f:id:sakamata:20181224151906p:plain:w300
A ∩ B

と表現される 俺要約 要は AND関数(かつ)で定義する範囲の結果を示す 記号の覚え方は 『蓋をした範囲のみ』でイメージ




はじめに

SQLは「集合指向言語」と呼ばれている、本書のテーマも集合論に基づいたSQLにある。しかし、SQLはちょっと前まで高校で習うレベルの集合演算子すら持っていなかった。
それゆえSQLは不完全なものという批判があった。

集合 SQL 採用Ver 記号
UNION 86~ A ∪ B
交差 INTERSECT 92~ A ∩ B
EXCEPT 92~ A - B
除算 DIVIDE BY 未対応 A / B ?

しかし、現在ではSQLに基本的な集合演算子が出揃い、本格的な応用も可能となってきた。
この章では、集合演算を利用したSQLを紹介し、違った角度からSQLの本質に迫る。



導入---集合演算に関するいくつかの注意点

要は SQLの tableやview を引数に取って演算をする。中学・高校で習った集合台数と似ているが、独特な特徴もあり注意が必要。


注意1

SQLの扱う集合は重複行を許す多重集合。それに対応するALLオプションが存在する。

通常の学習における『集合論』は重複を認めないが、SQLは認める前提 多重集合(multiser, bag)である。
UNION, INTERSECT をそのまま使うと結果から重複行を排除する。
重複行を残したい際は ALL オプションを使う事(例: UNION ALL )
SELECT句のDISTINCTと反対の扱いだが これは間違い→ UNION DISTINCT

ALL オプションを付けるとパフォーマンスが向上する (ソートが行われない為)
付けないと重複を許容し、暗黙的なソートが発生するので重くなるらしい。
なのでパフォーマンスチューニングでじゃ ALLオプションを付けると良い


注意2

演算の順番に優先順位がある

UNION,EXCEPT に対して INTERSECT の方が先に実行される。
もしUNIONを優先的に実行したい場合は (カッコ) でくくって順序を指定する事(後で詳細あり)


注意3

DBMSごとに集合演算子の実装にバラツキがある MySQLは2018年現在 INTERSECT,EXCEPT サポートしてない。
Oracle は EXCEPT が MINUS という名になっている


注意4

除算の標準的な定義がない

  • 和 UNION
  • 差 EXCEPT
  • 積 CROSS JOIN

はあるが、

  • 商 DIVIDE BY
    は、標準化が遅れている。
    (詳しくはP105 HAVING句の力 を参照の事)
    除算を行う際は自前でクエリを作る必要がある等



テーブル同士のコンベア---集合の相等性チェック[基本編]

さて、では実践編
下記の2つのtableの中身が全て等しいか検証するには?

tbl_a
+------+-------+-------+-------+
| key  | col_1 | col_2 | col_3 |
+------+-------+-------+-------+
| A    |     2 |     3 |     4 |
| B    |     0 |     7 |     9 |
| C    |     5 |     1 |     6 |
+------+-------+-------+-------+

tbl_b
+------+-------+-------+-------+
| key  | col_1 | col_2 | col_3 |
+------+-------+-------+-------+
| A    |     2 |     3 |     4 |
| B    |     0 |     7 |     9 |
| C    |     5 |     1 |     6 |
+------+-------+-------+-------+

--等しいテーブル同士のケース

DELETE FROM Tbl_A;
INSERT INTO Tbl_A VALUES('A', 2, 3, 4);
INSERT INTO Tbl_A VALUES('B', 0, 7, 9);
INSERT INTO Tbl_A VALUES('C', 5, 1, 6);

DELETE FROM Tbl_B;
INSERT INTO Tbl_B VALUES('A', 2, 3, 4);
INSERT INTO Tbl_B VALUES('B', 0, 7, 9);
INSERT INTO Tbl_B VALUES('C', 5, 1, 6);


[Extra おさらい] UNION の簡単な使用例

    SELECT * FROM tbl_a
UNION
    SELECT * FROM tbl_b;


    SELECT * FROM tbl_a
UNION ALL
    SELECT * FROM tbl_b;

UNIONだけを使う方法

2つのtableが完全同一か確認したい。しかしレコード数が多い場合は以下のクエリが便利

SELECT COUNT(*) AS row_cnt
  FROM ( SELECT * 
           FROM   Tbl_A 
         UNION
         SELECT * 
           FROM   Tbl_B ) TMP;

SQL文末のTMP は仮で定義するtable名みたいなもの。 適当にhogeでもOK
このクエリの結果が tbl_a と tbl_b の行数と一致すれば、両者は等しいテーブル

SELECT count("key") from  tbl_a;  => 3
SELECT count("key") from  tbl_b;  => 3

--「B」の行が相違するケース

DELETE FROM Tbl_A;
INSERT INTO Tbl_A VALUES('A', 2, 3, 4);
INSERT INTO Tbl_A VALUES('B', 0, 7, 9);
INSERT INTO Tbl_A VALUES('C', 5, 1, 6);

DELETE FROM Tbl_B;
INSERT INTO Tbl_B VALUES('A', 2, 3, 4);
INSERT INTO Tbl_B VALUES('B', 0, 7, 8);
INSERT INTO Tbl_B VALUES('C', 5, 1, 6);

再度 UNION のクエリを実行すると結果は 4 となり、2つのtableが異なる事がわかる。 例は * による全カラム対象だが、特定のカラムや where で特定の行範囲での比較も可能。

再度コレ

SELECT COUNT(*) AS row_cnt
  FROM ( SELECT * 
           FROM   Tbl_A 
         UNION
         SELECT * 
           FROM   Tbl_B ) TMP;

このクエリ文は以下の様な構造として読み取れる

Tbl_A UNION Tbl_B = TMP   
↓   
S UNION S = S   

これは数学でいう冪等性(べきとうせい idenpotency)と呼ばれる性質のもの
プログラムでは「繰り返し処理をしても最初の一度に実行したものと結果が同じになる」という事柄のニュアンスで使われる。

  • C言語 ファイルインクルードを何度しても結果が変わらない特質設計
  • HTTPのGETコマンド 同じ要求を繰り返し発行しても大丈夫な様になっている。
  • UIのボタンを連打しても1回として認識する

これらは冪等と言える
また、この式は2つだけの比較ではなく 3個以上の値でも比較可能。

S UNION S UNION S UNION ... UNION S = S

しかし、 UNION ALL テメーは駄目だ。
重複したら冪等性が失われる。したがって冪等性を確保するためにも ユニークキーは大事だね。 という事らしい。


おさらい 積集合 intersection

『両方ともにはいっているもの』を集めた集合
共通部分 ともいう
f:id:sakamata:20181224151906p:plain:w300

A ∩ B



テーブル同士のコンベア---集合の相等性チェック[応用編]

集合の相等性を調べる公式
1. (A ⊆ B) かつ (A ⊇ B) ⇔ (A = B) 2. (A ∪ B) = (A ∩ B) ⇔ (A = B)

(ド・モルガンの法則に近い理屈)

ここでは2番を使う 2番をSQLに訳すると

A UNION B  =  A INTERSECT B  なら AとBは等しい となる。

そして

A UNION B =  A=B       ならば
A INTERSECT B =  A=B   が成り立つ

二つのテーブルが相等なら「等しい」、そうでなければ「異なる」を返すクエリ

SELECT CASE WHEN COUNT(*) = 0 
                     THEN '等しい'
                     ELSE '異なる' END AS result
  FROM ((SELECT * FROM  Tbl_A
         UNION
         SELECT * FROM  Tbl_B) 
         MINUS  --  EXCEPT は oracle  では MINUS
        (SELECT * FROM  Tbl_A
         INTERSECT 
         SELECT * FROM  Tbl_B)) TMP;

相違した行を表示する。(oracleでerrorとなって確認できず)

  --テーブルに対するdiff:排他的和集合を求める。 
  (SELECT * FROM  Tbl_A
     MINUS -- EXCEPT
   SELECT * FROM  Tbl_B)
   UNION ALL
  (SELECT * FROM  Tbl_B
     MINUS -- EXCEPT
   SELECT * FROM  Tbl_A);



差集合で関係除算を表現する

現在のSQLには除算の演算子が無い為以下の方法などで自前でクエリを書く必要がある。

  1. NOT EXSIST を入れ子にする
  2. HAVING句を使った一対一対応を利用する
  3. 割り算を引き算で表現する

ここでは3番の方法を紹介

以下の様なtableがあるとする。

select * from Skills;
+--------+
| skill  |
+--------+
| Java   |
| Oracle |
| UNIX   |
+--------+

select * from EmpSkills;
+-----------+--------+
| emp       | skill  |
+-----------+--------+
| 平井      | C++    |
| 平井      | Oracle |
| 平井      | PHP    |
| 平井      | Perl   |
| 平井      | UNIX   |
| 渡来      | Oracle |
| 相田      | C#     |
| 相田      | Java   |
| 相田      | Oracle |
| 相田      | UNIX   |
| 神崎      | Java   |
| 神崎      | Oracle |
| 神崎      | UNIX   |
| 若田部    | Perl   |
+-----------+--------+

ここからSkills Tableにある全ての技術(Java,Oracle, UNIX)に
精通した社員を探すクエリはどう書けば良いか?

以下が答え しかし、注意!
MySQLは2018年現在 INTERSECT,EXCEPT サポートしてない。
Oracle は EXCEPT が MINUS という名になっている
Oracle LiveSQLで実践)

--差集合で関係除算(剰余を持った除算)
SELECT DISTINCT emp  -- empの重複をまとめる
  FROM EmpSkills ES1
 WHERE NOT EXISTS       -- サブクエリ結果の否定のものを該当させる
        (SELECT skill
           FROM Skills
         EXCEPT        -- 差集合 引き算をさせる
         SELECT skill
           FROM EmpSkills ES2
          WHERE ES1.emp = ES2.emp);

--oracleの場合は EXCEPT が MINUS となる
SELECT DISTINCT emp
  FROM EmpSkills ES1
 WHERE NOT EXISTS
        (SELECT skill
           FROM Skills
         MINUS          -- oracle の場合の差集合
         SELECT skill
           FROM EmpSkills ES2
          WHERE ES1.emp = ES2.emp);


解説

サブクエリ内(カッコ内)でしている事
平井さんの場合

| skill  |
+--------+
| Java   |
| Oracle |
| UNIX   |

引くことの(EXCEPT,MINUS)   
| 平井      | C++    | 無関係
| 平井      | Oracle | 消える
| 平井      | PHP    | 無関係
| 平井      | Perl   | 無関係
| 平井      | UNIX   | 消える

= Java が残る

相田さんの場合

| skill  |
+--------+
| Java   |
| Oracle |
| UNIX   |

引くことの(EXCEPT,MINUS)
| 相田      | C#     | 無関係
| 相田      | Java   | 消える
| 相田      | Oracle | 消える
| 相田      | UNIX   | 消える

= 空集合 Φ つまりここでは該当しない

このサブクエリの結果の NOT EXISTS つまり否定なので、空集合となった結果を表示させる。
すなわち、全てのスキルを持つ社員のみ表示。となる。

EMP
相田
神崎



等しい部分集合を見つける

以下のtableから数も種類も全て同じ部品を扱う供給業者のペアを求めるにはどうしたた良いか?
一応答えは AとC, BとD となっている

+-----+--------------+
| sup | part         |
+-----+--------------+
| A   | ナット       |
| A   | パイプ       |
| A   | ボルト       |
| B   | パイプ       |
| B   | ボルト       |
| C   | ナット       |
| C   | パイプ       |
| C   | ボルト       |
| D   | パイプ       |
| D   | ボルト       |
| E   | ナット       |
| E   | パイプ       |
| E   | ヒューズ     |
| F   | ヒューズ     |
+-----+--------------+

実は結構難しいパズル、理由はSQLには∅の包含関係や相当性をテストする記述がない為、集合と集合を一発で比較出来る書き方は出来ないので、ひねって答えを求める必要がある。 IN は要素として含まれるかを探す( ∈ )に相当する。 集合と集合に使うべき状態 ( ⊂ )足り得ない。
つまり、最初の比較対象である『まず定義すべき集合』が無い状態で、なんとかして、同一のものを探さないといけない訳。

ではどうするか?
まずは非等値結合で sup の組み合わせの総当たり表を作る。

SELECT SP1.sup as s1 , SP2.sup as s2
  FROM SupParts SP1, SupParts SP2 
 WHERE SP1.sup < SP2.sup              -- 業者の組み合わせを作る
GROUP BY SP1.sup, SP2.sup;

すると以下の様な総当たり表が作られる

+----+----+
| s1 | s2 |
+----+----+
| A  | B  |
| A  | C  |
| A  | D  |
| A  | E  |
| B  | C  |
| B  | D  |
| B  | E  |
| C  | D  |
| C  | E  |
| D  | E  |
| E  | F  |
+----+----+

次にこのペアについて
(A ⊆ B) かつ (A ⊇ B) ⇔ (A = B) の公式を当てはめる

SELECT SP1.sup as s1 , SP2.sup as s2
  FROM SupParts SP1, SupParts SP2 
 WHERE SP1.sup < SP2.sup
   AND SP1.part = SP2.part            -- 条件1.同じ種類の部品を扱う
GROUP BY SP1.sup, SP2.sup 
HAVING COUNT(*) = (SELECT COUNT(*)    -- 条件2.同数の部品を扱う
                     FROM SupParts SP3 
                    WHERE SP3.sup = SP1.sup) -- s1カラムで数が同じものを探す
   AND COUNT(*) = (SELECT COUNT(*) 
                     FROM SupParts SP4 
                    WHERE SP4.sup = SP2.sup); -- かつ s2カラムで数が同じものを探す

うーん、かなりややこしい…。
筆者もパズルと言っているので、無理に理解しなくともよいかと
上記クエリの結果

+----+----+
| s1 | s2 |
+----+----+
| A  | C  |
| B  | D  |
+----+----+

しかし、もし将来的に集合の除算が可能になったら以下のようなクエリで書けるかもしれないらしい。

SELECT 'A CONTAINS B'
  FROM SupParts
WHERE ( SELECT part
          FROM SupParts
        WHERE sup = 'A')
    CONTAINS   -- こんなのあったらいいな、ということらしい
      ( SELECT part
          FROM SupParts
        WHERE sup = 'B')

業者Bび扱う部品全てを業者Aが扱っていれば 'A CONTAINS B' と表示させる。
というクエリがあったらなー。みたいな話



重複行を削除する高速なクエリ

再度、3 自己結合の使い方[P49 重複行を削除する] で使った tableで重複行を削除する問題を扱う。

Products table
+-----------+-------+
| name      | price |
+-----------+-------+
| りんご    |    50 |
| みかん    |   100 |
| みかん    |   100 |
| みかん    |   100 |
| バナナ    |    80 |
+-----------+-------+

3 自己結合の使い方 に出ていたクエリを再度確認
※ rowid はMySQL使えないので注意

--重複行を削除するSQL :相関サブクエリの利用
DELETE FROM Products
 WHERE rowid < ( SELECT MAX(P2.rowid)
                   FROM Products P2
                  WHERE Products.name = P2.name
                    AND Products.price = P2.price ) ;

上記はパフォーマンスに問題あり、相関サブクエリを使わない方法を求める。
今度は削除対象となるrowidをサブクエリ内で全部求めてしまう。

--高速版1:削除すべき行のrowidを求めて、その行を削除
DELETE FROM Products 
 WHERE rowid IN ( SELECT rowid                 -- 全体のrowid
                    FROM Products 
                   EXCEPT -- oracleなら MINUS   引く
                  SELECT MAX(rowid)            -- 残すべきrowid
                    FROM Products 
                   GROUP BY name, price);      -- グループ複数定義で一意性を求める
table 全体
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |

引くことの
残すべきID
| 1 |
| 4 |
| 5 |

残った削除対象のID
| 2 |
| 3 |
--高速版2:残すべき行のrowidを求めて、それ以外の行を削除
DELETE FROM Products 
 WHERE rowid NOT IN ( SELECT MAX(rowid)
                        FROM Products 
                       GROUP BY name, price);



まとめ

  1. SQLでは集合演算機能の整備が遅れている、使える内容もDBによって異なるので注意が必要
  2. 集合演算子はALLオプションを付けないと重複排除を行う。その際、ソートも行われるので、パフォーマンスが悪くなる。
  3. UNION,INTERSECTは、冪等性を持つ、 EXCEPTは持たない
  4. 除算は標準的な演算子が無いので自前で作る必要がある。
  5. 集合の相等性を調べるには、冪等性か全単射を利用する2通りがある
  6. EXCEPTを使うと、補集合を簡単に表現できる



演習問題

省略

プログラミングを続ける心構えみたいなもの

プログラムのスキルを付けて行くためにコツコツやってきた際の心構えみたいなものをちょっとまとめます。 これは何も皆さんにどうこう言って上の年齢からマウント取りたいとかそういう類の事じゃなくて、自分がブログを続けて行く際に、プログラミングを学ぶ際に身に付けた事を、今度はブログの定期的な記事アップに役立てたいと思ったものをメモ程度にアップしようと思ったためです。自分メモの色合いが強いですが、何かの参考になれば幸い。

以前はてな匿名ダイアリーで読んだすげえ良い記事があって、そこにこんな様な事が書いてありました。という要約。
でも記事のクリップを思い出せない。でも、その記事を記憶を頼りに自分なりに要約するとこんな感じ。

ちょっとでも実行したらその日はOKとする。欲は出さない。

何かを成し遂げるなんてただでさえ凄すぎるんだから、構えちゃ駄目。毎日なんにもしないし、出来なくて終わるのを当たり前の状態に思うのがまず大事。
そこからまかり間違って、ちょっとでもなんか出来たらそれで大成功と思うようにする。
例えば、プログラミングやりたいなーと思ってたら、それに関する記事を読むだけでもOKにしてしまう。
もしまかり間違ってエディタ開いたりなんかしたら大成功だし、1行でもなんか書いたりコメントだけでも書いて保存すればその日はもうやんなくても良い。自分の勝ち。と思っていい。
でも、たぶんそれだけじゃ満足いかなくなる時があって、そういう時は飽きたりしんどくなったりする所までやってみる。めんどくさくなったりしんどくなったら遠慮なく辞める。
たぶんこれって物事を嫌いにならないコツにも通じてると思う。
それを、あくまでも自分のプレッシャーにならない程度に続ける。たまたま一月や二月なんにもしない日があっても、絶対に自分を責めちゃだめ。だってできなくて当たり前なので。
それでやりたい時だけやってみる事を繰り返す。
そのうち徐々にやっている時間が増えてきたらこっちのもの。それを嫌になる手前まで毎回やればいいだけ。

構えない、デキは悪くて良い。

当然、できたものは、出来の良し悪しとかも気にしなくて良いし。他人の批評も知ったこっちゃないし、途中で投げ出しているものでも良い。だって何もない所からそこまでできただけで凄いのだから。ゼロからスタートしてそこまで行ったのって本当にすごいので、それを自分でけなすのは絶対にしない。ていうかそこまでできたお前すげえな。って自分をホメると良い。

みたいな内容のものだった。
当然記憶の彼方で自分の要約や解釈がたぶんに入ったものになっているが、つまりはそういうことをいつの間にか繰り返しやって来ただけかもしれない。 で、ここから後は自分なりに心がけてきた追加の事柄。

ほんのちょっとでもやってみるを繰り返す

出来なくて良い。というのが上の内容だったりするけど、そこから一歩だけ進めて、毎回ほんのちょっとだけ、こう思うようにする。「ほんのちょっとだけやってみる」
つまりエディタ開いたり、仮想環境立ち上げたりするだけでもOK、あと、今やんなきゃ!って所と全く関係ない、楽で簡単な所をちょっといじるだけでもOK。面倒だなぁ。と思っている時に、よくやるのが文言やデザインの色とか空間の空き方をちょっとだけ変える。みたいな事。これだけでも出来たら十分と思う。

めんどくさくなったらなんにもしないか別の事をやる

処理が上手くいかなかったり、参考にしている書籍や記事の書いてある内容が理解できなくて「あーっ!」ってなったら無理しないで投げ出す。というか、ふて寝する。
もし、余裕があれば気分転換にWebのアホっぽい記事読んだり、散歩や遊んでも良いけど、最近は欲が出てきて、実装でハマったり一区切りついたら、記事を書く、とか、プログラム以外の何かをする。とかより生産性の高い事をしてやろうと思う自分がいるけど、頑張りすぎない様に適当に遊びを入れる様にもう一人の自分に監視をさせて適時、適当に遊ぶようにもしているし、最近は別の生産的な事もしようとしてる。このバランスを保つのが結構難しいけど、基本はやっぱりできなくて当たり前。の超低い所にハードルを設けるのが良いみたいです。

処理なんか動けばいい、汚くても良い

お客さんの付くサービスだとセキュリティをしっかりしたり動作を保証できるものにする必要がありますが、自分で趣味で作ったり、学習のために作っている様なものは、とにかく動けば勝ちだと思うようにしてます。プログラミングってある程度やって行くと、ルールや作法や思想的なものが多分にあって、どれを選ぶべきか?みたいな事に悩んだりすることもありますが、そんなのは後の後、必要になった時に仕方なくやれば良いと思ってます。目的は続けることなので、作法やルールに縛られて自分の中から沸くやる気を削ぐような事だけはしない方が良い。位に構えている方が、結果として続けられてより良いものを書こうという気持ちにも繋がるような気がします。

判んなくていい、わかる範囲でやれば良い

プログラムって基本的な処理の組み合わせだを愚直に続けてもそれなりに動いたり色んな事が出来たりするので、あえて高等なテクニックや処理などをして自分を苦しめる必要も無いと思ってます。でもいずれどうしてもそれをやらないと駄目な処理とかが出てくるのでその時に改めて新しい技を使えば良いのかなと思います。

ここは後で直すとかコメントに書いて放置する

かなり具体的になってきましたが、一人で書いていることがほとんどなので、遠慮なくこういうのを書いて他の部分を作り込んだりしてます。
***ToDo*** とか書いたまま放置してる所が山の様にある。

俺俺タスク管理をする

時間とかにあまり縛られないタスク管理表を自分なりに作ってます。作るべき項目を箇条書きでスプレッドシートにまとめて、適時項目を追加したり、終わったものをグレーアウトさせて表の下の方に移動させる。というものをしてます。見積もり時間と実装時間の項目も設けてますが、あくまで実感に伴う時間を書く程度にとどめ、厳密に書かない事が上手くいっているコツな様な気がしてます。また、優先順位も設けてますが、これもあまり厳密に管理せず、今必要だと思ったものや、ノってきた際には、優先度が低くてもそれを先にやる様にしています。優先度高いのにずいぶん放置してるものがあっても気にしない。ただ、セキュリティの部分やデータに異常が起こる可能性のあるものの処理だけは、優先度を高くしてちゃんとやってます。

ざーっとまとめたけど最後にこれも大事だなーと思うのがあった

精査しないで良い。ひとまず出しちゃう

という訳でこの記事もあまり文章校正や言い回しをチェックしないままアップしてみます。
言い回しとか文字の間違いを気にし過ぎると、めんどくさくなってアウトプットが億劫になる位ならほいほい出してしまう。というのも大事かもしれません。

関連記事: sakamata.hateblo.jp

Laravel フォームで配列を扱う ヘルパ関数old() でチェックボックスを扱う方法

最初の記事がとてもバズり、おかげ様でこの週のはてなブログのランキングに乗ることができました。ありがとうございます。
今回は具体的な技術のTIPSエントリーとなります。こんな感じの記事の時もありますし、おっさんらしく蘊蓄をたれたり心構え的なものや日記みたいな事も記事にしていこうと思ってますので、よかったら今後もお付き合いいただければ幸いです。





フォームのチェックボックスで、DBの boolean 値 を変更させるユーザーフォームを作っていた際、ヘルパ関数の old() やPOSTすべき値で散々悩んだので、うまくった例を記述します。

実装したフォームの参考画像
実装例、『非表示』とあるチェックボックスのバリデート時にリダイレクトされた際に、ユーザーのチェックした内容をどう保持するか、という問題です。

対象はユーザーフォームの 『端末を非表示にする』 というチェックボックスカラム名は hide で 1なら非表示 0なら表示としてます。 しかもこのフォームでは複数端末が表示されるので、配列で扱うというめんどくさい状態。さらに言うなら、DBの値を引っ張りだしてきて、 true なら checkd="checked" を表示させる。 さらに バリデートエラーの際はヘルパ関数 old() でチェックの状態を保持する、という  boolean が何重にも絡んでくるややこしい状態で問題の整理にかなりの時間を要しました。
前提条件としてhtmlのformで配列を扱う際には foreachなどで連続して同じ様なformを出力する必要がある場合があります。
その際に name等は以下の様に配列として扱います。
name="hide[]"

また、連想配列で扱いたい際は
name="hide[key]"
と、書いて扱います。

参考サイト
FormのPOST送信で配列を、できれば連想配列を送信したい。

以上を踏まえた上で先に結論です。

@php
if(
    ($mac_add->hide == 1 && old('mac_address.'.$mac_add->id.'.hide') == null) ||
    old('mac_address.'.$mac_add->id.'.hide') == 1
) {
    $check_hide[$mac_add->id] = "checked='checked'";
} else {
    $check_hide[$mac_add->id] = "";
}
@endphp
<!-- チェックされていない場合は0を送信 -->
<input type="hidden" name="mac_address[{{$mac_add->id}}][hide]" value="0">
<input type="checkbox" name="mac_address[{{$mac_add->id}}][hide]" value="1" id="devise-check-{{$mac_add->id}}" {{$check_hide[$mac_add->id]}}>

viewに @php で直接設定書いてますが、さんざん悩んだので、これ以上いじりたくないという状態です。 あと、三項演算子が苦手で、htmlの中にIF文書きたくないのです。

このphpコードは要は最初のifで チェックボックスに印をつけるか否かの判定をしているのみです。
$check_hide[] という配列に html の"checked='checked'" を書くべきか否かを処理してます。

以下はヘルパ関数 old() で取れる hide の配列内の値です。
'mac_address.'.$mac_add->id.'.hide'

これは、以下の様に解釈します。
array[id][hide]

端末情報 mac_address の配列情報の中に端末の固有IDをキーとして配列を保持させ、さらにその中の hide キーの中身を呼び出してます。
じつは、array[][hide] という風に配列を空にして、キーを自動で入る通し番号にしても良い気がしますが、controller側に渡して処理する際に 配列キーがDBのプライマリキーのIDだと、何かと都合が良いのでこうしてます。

この値はバリデートエラーなどでセッションに初めて保持される値で、普段は null です。
従って以下は フォームの値が 1 の状態か、 old() の値が無いなら、 true となります。
($mac_add->hide == 1 && old('mac_address.'.$mac_add->id.'.hide') == null)

さらにif関数の論理式でor条件を立ててます。
old('mac_address.'.$mac_add->id.'.hide') == 1

単純に、 old の値で 1、つまり true が入っているかを判定。 このいずれかに該当すれば、非表示状態の設定とみなし "checked='checked'" がフォーム内に描画され、チェックが入ります。

さらに下のhtmlフォームです。

<!-- チェックされていない場合は0を送信 -->
<input type="hidden" name="mac_address[{{$mac_add->id}}][hide]" value="0">
<input type="checkbox" name="mac_address[{{$mac_add->id}}][hide]" value="1" {{$check_hide[$mac_add->id]}}>

チェックボックスの前の行にhiddenでcheckboxに同じ name="" を付けてますが、これがちょっとややこしい事になります。 知っている人には当たり前かもしれませんが、実はチェックボックスのフォームって、チェックを入れないと、POSTされないそうです。 つまり、このままですと、ユーザーが空のフォームにチェックした情報は取れますが、ユーザーが意図的にチェックを外した。という情報が取れない訳です。 そこで、 hiddenを chekboxの前に書いて、もしユーザーがチェックを入れてない場合は、 name="hide" に 0 の値をPOSTしてやる、という事をしています。 htmlフォームで name が同じものが2つ以上あった場合は、後に書いた方の値が飛ぶ、という仕様をハックして、普段は下のフォームの値が飛び、いざチェックが外された際はhidden の値が飛ぶ、という訳です。

詳しくは以下を参照
[HTML]formでcheckboxにチェックしていない時にもパラメータを送る方法 isket.jp

これで各フォームの name="hide" に 0と1の状態がPOSTされる状態が表現できました。あとはバリデートエラー等で、再度フォームを描画する際に old 関数の値を引っ張り出すことで、フォーム直前の状態が 0か1がわかり、それを元に "checked='checked'" を入れるか入れないかを判定すれば良い。ということですね。

参考サイト

Laravelで多次元配列のバリデーション
qiita.com

checked="checked"はどうやって表示させるの? qiita.com

Laravelで、チェックボックスをPOSTした後の old() 問題 qiita.com

次回は軽めの記事にしようと思います。

40歳を過ぎてからプログラミングを始めて滞在者確認サービスをリリースするまでの話

こんにちは世界。このたびシェアオフィスやコワーキングスペース等で、滞在者が確認できるサービスを制作しリリースしました。既に複数のコミュニティで使用をしてもらっており、今後も広く皆さんに使ってもらおうと思ってます。

このブログでは40歳を過ぎてプログラミングを始めた顛末や、技術的な方法論の共有、備忘録、またサービスの発展や、今後行って行きたい事柄についてなるべく気軽に書いて行こうと思います。

 

コンセプト

 誤字脱字気にしません。

 事実誤認のご指摘あれば直します。

 文章で議論とかはしません。

 気軽に、軽めに、自分の負担にならない形で記事を書きます。

 

今回はサービスの概要とプログラミングを初めてからサービスをリリースするまでの経緯を書きます。

 

どんなアプリなのか?

任意の人がある程度自由に集まる場所で『今、誰がいるか?』が、スマホに通知され確認できるサービスです。シェアオフィスやコワーキングスペース、またはボードゲームのプレイスペース等、様々なコミュニティで使ってもらえるものを目指しています。

f:id:sakamata:20181116072626p:plain

 

どんな仕組と方法なのか?

シェアスペースにあるwi-fi に来訪者がスマホやパソコンでネットに接続している際に、端末に付いている固有番号『MACアドレス』というものを、他のパソコンで定期的に確認し『このMACアドレスは○○さんのスマホ』というように、端末と人を結び付けて、今その場所に誰が居るかがわかる仕組になっています。

他の人の端末の固有番号、MACアドレスは、wi-fiに接続していると、実は誰でも見る事が出来ます。コマンドプロンプトという黒い画面を出して arp -a と打つだけですが、wi-fiや端末の設定などで見れない場合もあります。

しかし、これは、いつ、どこに誰が居たかを特定できる個人情報になりますので、確認の為に収集したMACアドレスは、不可逆暗号化されています。なので誰にもMACアドレスが判らない形でありながら、個別の端末判定ができる様になっています。

f:id:sakamata:20181116181515p:plain

自宅のwi-fi環境でコマンドを打った例です、私は別にバレてもいいのですが…。

 

余談ですが、この問題解決の為にteratailで質問したら、なんと徳丸本の中の人(体系的に学ぶ 安全なWebアプリケーションの作り方 等の著者)からアドバイスをもらえて嬉しかった。teratail.com

 

MACアドレスの確認には数千円の小型パソコン RaspberryPi を使用しています。タバコの箱より小さくて、消費電力はおよそ2.5Wと電球以下、オフィス等にモニターもマウスも無しでコンセントに繋げるだけで動きます。これをwi-fiネットワークに繋ぎ、今現在接続されている端末を24時間調べ続けて、インターネット上のサービスに情報を送信しています。

f:id:sakamata:20181116181801p:plain

 (エンジニアの方向けの概要説明)具体的には arp-scan というコマンドでwi-fiネットワークをポーリングしてます。cronで1分おきにコマンドを叩き、接続端末に変化があればshellスクリプトで取得した値をJSONにして、hashキーを付与してWebサーバーに側にhttps curlでPOSTしてます。

 slack sampleç»å

Webサーバー側は、いわゆる普通のWebアプリになっており、PHPフレームワークLaravelとMySQLで構成しています。管理者が端末番号と人を結び付けたり、ユーザーが自分のプロフィールや端末の管理ができる様になっています。

また、人が来たり帰ったりすると、皆さんが良く使うLINEやメール、Slack等のおなじみのコミュニケーションアプリに通知してくれるので、ほぼリアルタイムにお知らせが来ます。具体的にはIFTTTのwebhooksを使ってます。

f:id:sakamata:20181117044618p:plain

f:id:sakamata:20181116072823p:plain

また、一覧で誰が居るかを知りたい場合は、コミュニティのメンバーだけに共有されている秘密のページで現状を確認できます。さらに来訪や帰宅を知られたくない人は、匿名や非表示にして存在を隠すこともできます。

f:id:sakamata:20181116181917p:plain

実際に稼働中のサービス画面です

今後はこのサービスを他のIoTデバイスや様々なサービスと連携させ、シェアリングエコノミーを運営する方や、そのコミュニティの参加者の方にとって便利で嬉しいサービスを総合的に提供して行きたいと考えています。人が集うための触媒になるようなサービスをたくさん作りたいですね。現在はそのプラットフォームとしてのアプリという位置づけでやってますので、よかったら使ってやってください。

販売受付のサイトはこちらです。

https://www.livelynk.jp/

 

なお、現状ソースコードGithubOSSの状態で上げていますので、よかったらツッコミや協力をしてもらえるととても助かります。

 

github.com

 

 

github.com

 

作ることになったきっかけと感謝したいこと

長くギークオフィス恵比寿という招待制の会員オフィスのメンバーで、そこで『オフィスに今誰が居るかを知りたい』というニーズがありました。しかし、カメラとかだとプライバシー的にも心理的にも監視されているようで嫌なわけです。そこで、IoTデバイスとかでなんとかなんない?という相談がきっかけになり、試行錯誤の末にこのような仕組みに落ち着きました。

実はここのメンバーには大変お世話になっており、サービスのwebデザインをやってもらったり、企画やプロデュース、少額個人投資もしていただきました。また、最近では営業等を行ってくださる方も現れて、自然発生的に様々な方にゆるく手伝って頂いてます。このつながりが無ければこのサービスも、私のプログラミング技術もここまで来れませんでした。日々感謝してます。そして私のもっとも身近な人に最大限の感謝を。今まで大変な迷惑をかけてきたので今後はちゃんと恩を返して行きたいです。

 

私のこと

50歳近いおっさんで、40歳もなかばに近づいた頃にプログラミングを本格的に始めました。普段は大学の非常勤講師で、パソコンの基礎(Word,Excel)をスマホ世代の大学生に教えたり、主に介護福祉系の中小企業やNPOさんのIT全般のサポート業務をしてますがまあ貧乏です。

プログラミングは主にPHP, JavaScript, 少しだけ Ruby on Rails を使います。VPSの構築やDBの最低限の設定はできる様になりました。現在はPHPフレームワークのLaravelを主に使ってますが、今後は linuxコマンドの詳細把握や、IoTデバイスとの連携、APIの活用や作成、node.jsでサーバーサイドJavaScriptや、可能なら以前挫折したPython等にも手を出していきたいと思っていますが、具体的に作りたいものがたくさんあるので、都度最低限の学習で最大限の効果を出せるものを模索したいタイプです。最近度数1.5の老眼鏡を買いました。

 

プログラミングを学んできた経緯など

プログラミング紀元前

十年以上前から思いついた仕組があって、それを実現したいと色々やってましたが、やっぱり自分で作れるようにならなきゃ駄目だな。という事が身に沁み始めたのが6年位前でしょうか?それまでプログラミングは経験全く無しでした。IT企業の営業から何故かSEやディレクションみたいな事をしつつも、いざプログラミング学習に挑戦しようとして何度も挫折してました。「英語の出る黒い画面怖い!」みたいな感じです。ただ、自分なりにWebサイトを作った事はあったのでhtmlやcssの基本位は把握していました。

 

初めてのPython 第3版

初めてのPython 第3版

 

 いきなりオライリーは無謀だと思った。

JavaScriptの絵本 ホームページ作りが楽しくなる9つの扉

JavaScriptの絵本 ホームページ作りが楽しくなる9つの扉

 

これでも難しかった。

 

プログラミング元年

それまでもちょいちょい初心者向けのJavaScriptの本を読んでifやfor位は試してはみていたのですが、それが一体なんでアプリになるのか?という所には結び付かず、ちゃんとしたものが作れるようになるには、果てしなく学習しなくてはいけないのか…。と愕然としていましたが、ある時 ITの講師としてMicrosoftAccessを教える事になったのがきっかけで、データベース(DB)に興味を持ち『基礎からのMySQL』という本を買った事で開眼しました。

基礎からのMySQL 改訂版 (基礎からシリーズ)

基礎からのMySQL 改訂版 (基礎からシリーズ)

 

私の起源でありバイブル

DB操作の基礎とPHPでごく簡単な掲示板を作る本でしたが、これがきっかけで『DBは要は表。プログラムで作るアプリケーションはその表に値を入れたり出したり、上手い事表示させるための仕組。』というDBを使ったアプリの基本概念が腑に落ちてからは、様々な事柄の理解がいっきに進みました。

 

プログラミング幼年期

いくつかの本を読み進めつつ、PHPのスクラッチでどうしても作りたいシステムをコツコツ作ってました。この辺の詳しい話はいずれ書きます。 ただ、理想の完成形は多分自分ひとりのスキルで作るのは難しいかも。とかも考えますが、どこまでいけるか一生かけてやってみたいです。

基礎からのPHP (基礎からシリーズ)

基礎からのPHP (基礎からシリーズ)

 
10日でおぼえるLinuxサーバー入門教室 CentOS対応

10日でおぼえるLinuxサーバー入門教室 CentOS対応

 
10日でおぼえるJavaScript入門教室 第3版 (10日でおぼえるシリーズ)

10日でおぼえるJavaScript入門教室 第3版 (10日でおぼえるシリーズ)

 

 これらで基礎的な事を徐々に身に付けてました。

プログラミング近代

『パーフェクトPHP』という本を買い、本にあったスクラッチのサンプルフレームワークを写経した後、それを拡張してどうしても作りたいアプリケーションのバージョン2を作ってました。MVCオブジェクト指向、クラスの使い方をなんとなく把握した感じです。

 

パーフェクトPHP (PERFECT SERIES 3)

パーフェクトPHP (PERFECT SERIES 3)

 

いまだに辞書替わりにしてます、フルスクラッチフレームワーク制作は手ごたえたっぷりでした。

 

この頃にポツポツと個人からのアプリ制作の依頼があり、個人事業主としてスポットで制作を請け負ったりしました。GoogleMAP Distance Matrix API から値を取得するプログラムを制作したり、リモートでとあるサイトの機能追加等のお手伝いをしたりしました。

改訂3版基礎 Ruby on Rails (KS IMPRESS KISO SERIES)

改訂3版基礎 Ruby on Rails (KS IMPRESS KISO SERIES)

 

フレームワークとして優れた設計思想というものを目にしたのと2つ目のサーバーサイド言語として知識が並列して役立つ事を学びました。

 

去年あたり

個人からの依頼がポツポツ来るようになり、様々な企画や提案をされた方のWebサービスのプロトタイプをスクラッチで何度か作る事になりました。この頃からちゃんとしたフレームワークを使おうと思い、Railsよりも慣れたPHPで「これから来るかも」と目を付けたLaravelの学習をしつつプロトタイプアプリを作ったりしてました。

PHPフレームワーク Laravel入門

PHPフレームワーク Laravel入門

 

 この辺までくるとノリでなんとなくサクサクできる様になりました。カスタマイズもリファレンスやブログ記事をググりながらやってました。

今年

どうしても作りたいアプリとかなり似たものを作ろうという話が一部で盛り上がり、少しだけ作ったりしてました。これは今も周囲に期待してもらっているので、今のサービスと並行して進めて行こうと思ってます。そして今年、8月初旬に『シェアオフィスの滞在者がわかるアプリが欲しい』という依頼を受け現在に至ります。制作期間は一か月で1つのコミュニティで使用する基本のモックアップが完成し、運用しつつアップデートしていました。3か月経った現在では複数のコミュニティで使って貰えるよう、DB設計を1から見直したものが動いてます。(DB周りを全て書き直しました)

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

  • 作者: 竹澤有貴,栗生和明,新原雅司,大村創太郎,丸山弘詩
  • 出版社/メーカー: ソシム
  • 発売日: 2018/09/26
  • メディア: 単行本
  • この商品を含むブログを見る
 

この本の輪読会で毎週秋葉原コワーキングスペースWeeybleさんに通わせてもらってます。サービスを作りつつ今はこれと格闘中です。内容はかなり難しく、2章で「Laravel何もわからない」状態ですが、読むほどにリファクタリングしまくりたくなる…。

 

これから

既に書いてますが、もっとたくさんのものを形にして行きたいです。あと、いい歳をした未経験の野生のプログラマーを雇ってくれる様な会社も無いと思いますが、プログラミングでほどほどにご飯が食べられるようになりたいです。また、今まで長く自分にはアイデアはあるけどできない事が山の様にあったので、それをどんどん作っていきたいですね。それらを作り、使ってもらう事で、私も、私に近しい人も、そして利用する方も、人生が少しでもラクで楽しくなれば良いなと思ってます。

 

はてなブックマークは10年以上使っているユーザーですが、はてなダイアリーをちょうど10年間放置してました。記念に10年前の記事?をインポートしています。が、良い機会なので今後は出来る範囲で気軽な情報のアプトプットをして行こうと思います。よかったら今後も更新する記事を読んでもらえると嬉しいです。