Friction River Software

  • お問い合わせ

CakePHP5入門【WebAPI編⑤】管理用ページ②

A子

今回って、Excelファイルの内容を元にデータベース登録していく回だね

まずは「AdminController.php」の中で「Numbers」クラスを使えるようにしよう

C菜

それでは、テーブルをフェッチしますね~

class AdminController extends AppController
{
    private $Numbers;

    public function initialize(): void
    {
        parent::initialize();

        $this->middleware(new HttpBasicAuthMiddleware());
        $this->loadComponent('SixSevenWeek');

        $this->Numbers = $this->fetchTable('Numbers');
    }

赤字部分が追加した箇所です~

A子

んじゃ、「import」メソッド内に追記していくよ

//データベース登録
$numbers = $this->Numbers->newEmptyEntity();
$numbers->lottery_time = $lottery_time;
$numbers->lottery_date_dt = $lottery_date_dt;
$numbers->lottery_date_str = $lottery_date_str;
$numbers->lottery_date_year = $lottery_date_year;
$numbers->lottery_date_month = $lottery_date_month;
$numbers->lottery_date_day = $lottery_date_day;
$numbers->lottery_week_int = $lottery_week_int;
$numbers->lottery_week_str1 = $lottery_week_str1;
$numbers->lottery_week_str2 = $lottery_week_str2;
$numbers->lottery_rokuyo_int = $lottery_rokuyo_int;
$numbers->lottery_rokuyo_str = $lottery_rokuyo_str;
$numbers->num3_str = $num3_str;
$numbers->num3_int = $num3_int;
$numbers->num3_place1 = $num3_place1;
$numbers->num3_place10 = $num3_place10;
$numbers->num3_place100 = $num3_place100;
$numbers->num4_str = $num4_str;
$numbers->num4_int = $num4_int;
$numbers->num4_place1 = $num4_place1;
$numbers->num4_place10 = $num4_place10;
$numbers->num4_place100 = $num4_place100;
$numbers->num4_place1000 = $num4_place1000;
if (!$this->Numbers->save($numbers)) {
    $this->Flash->error('ナンバーズファイルのデータベース登録に失敗しました。');
    return $this->redirect(['action' => 'index']);
}

これをforeachループ内の末尾に書くよ

B美

ちょっとちょっと!
エラーになったとき、いきなりリダイレクトしちゃダメよ

ループからbreakしたあと、しかるべき処理を行わないと…

C菜

あ~
例えば、10件目の処理でエラーになったときは、最初の1件目から9件目までを元の状態に戻さないといけないのでしょうか~?
(要するに「全て無かったことにする」ということです~)

B美

今回のケースでは問題ないわ(そこまでしなくてもOK)
抽選の各回は独立していて、前後の関係性は無いからね

でも、せっかくだから(練習のために)「トランザクション処理」を実装してみましょう
(途中でエラーになったら、全て「無かったことにする」の)

A子

とらんざくしょん?

それって、「トランザクションテーブル」とは違うの?
(【CakePHP5基礎編⑦】を参照)

B美

「トランザクションテーブル」ってのは「マスターテーブル」の対義語で、更新頻度が高く、参照頻度が低いテーブルのことね

ここでの「トランザクション処理」というのは、「分離できない一連の処理」のことよ

C菜

よくわからないです~

B美

例えば、そうねぇ

口座】テーブル
口座番号預金額
1234100000
567830000

というテーブルがあったとして、1234番の口座から5678番の口座へ50,000円の振込処理を行うとしましょう

A子

結果はこうなるね

口座】テーブル
口座番号預金額
123450000
567880000

C菜

1234番は50,000円減って、5678番のほうは50,000円増えてます~

B美

このときデータベース処理としては2件になるのよ

①1234番の口座の預金額を100000から50000に更新
②5678番の口座の預金額を30000から80000に更新

この二つって、どちらか一方だけ成功して、一方は失敗って状態は許されるかしら?

A子

あぁ、なるほどねぇ
①が成功、②が失敗の場合

口座】テーブル
口座番号預金額
123450000
567830000

になっちゃうし、
①が失敗、②が成功の場合

口座】テーブル
口座番号預金額
1234100000
567880000

…って状態になるね

C菜

どっちもダメダメです~(笑)

たしかにこれらは「分離できない」処理ですね~

B美

そういうこと

何千件ものデータベース処理をループを回して行う場合、トランザクション処理にするほうが無難でしょうね
(処理速度も速くなるし…)

A子

ん?

速くなる?

B美

ええ
例えば、3件の処理を行うとして…

【更新処理①→①を確定→更新処理②→②を確定→更新処理③→③を確定
よりも
【更新処理①→更新処理②→更新処理③→①から③をまとめて確定

のほうが速くなるってわけ

C菜

具体的な手順はどんな感じになるのでしょうか~?

B美

そんなに難しくは無いわよ

1.トランザクションスタート
2.あれやこれやの処理
3.全て成功すればコミット、一つでも失敗すればロールバック

…って感じね
(要は、今までのデータベース処理にが加わっただけ)

A子

コミットってあれかな?

「結果にコミットする」

C菜

○イザップ~(笑)
(一部、伏字)

B美

あのテレビCMで言ってるコミットは「約束する」とか「引き受ける」って意味でしょうね

データベースで使うコミットは「委託する」なんかの意味になるのかな?
(よく知らん)

まぁ、トランザクション型データベースでは、一般的に「更新を確定する」って意味で使ってるけど…

C菜

ということは、ロールバックは「更新を無かったことにする」って意味ですか~?

B美

その通り

あ、「更新(UPDATE)」って言ってるけど、「挿入(INSERT)」や「削除(DELETE)」も含んでるからね

A子

うーん
たしかに便利そうな機能だね

ちなみに、どんなデータベースソフトにも備わってる機能なの?

B美

今の時代、関係データベースのほとんどは「トランザクション型データベース」じゃないかな?
(知らんけど…)

なにしろマイクロソフトの「Access」ですらトランザクション型なんだから…

C菜

それはともかくとして、実際のコードを教えてください~

B美

OK

$this->Numbers->getConnection()->begin();
で、トランザクションスタートして

$this->Numbers->save($numbers)
を複数件実行して、全て問題なく終了したら

$this->Numbers->getConnection()->commit();
コミット

$this->Numbers->getConnection()->rollback();
ロールバック

A子

なにそれ
超・簡単じゃん

//トランザクションスタート
$this->Numbers->getConnection()->begin();
$error_flag = false;

foreach ($data as $row) {

    ・・・

    if (!$this->Numbers->save($numbers)) {
        $error_flag = true;
        break;
    }
}

if ($error_flag == false) {
    //コミット
    $this->Numbers->getConnection()->commit();
} else {
    //ロールバック
    $this->Numbers->getConnection()->rollback();
}

これでどうよ

C菜

あと、最後のフラッシュメッセージも書き換えるです~

if ($error_flag == false) {
    $this->Flash->success($cnt.'件の処理を行いました。');
} else {
    $this->Flash->error('エラーが発生しました。');
}

ついでに、前回書いたデバッグ用のフラッシュメッセージについても全てコメントアウトしておきますね~





B美

ふむ、まぁ良いんじゃないかしら

それじゃ、テスト実行してみなさい

C菜

やってみますね~

えいっ!

A子

お、ばっちり成功したみたいだね

んじゃ、以下のSQL文を実行して確認してみよう
(本当は「select * from numbers」としたかったけど、表示が崩れちゃうんだよね(苦笑))

select id,lottery_time,lottery_date_dt,lottery_date_str from numbers where lottery_time between 6925 and 6929;
select lottery_date_year,lottery_date_month,lottery_date_day from numbers where lottery_time between 6925 and 6929;
select lottery_week_int,lottery_week_str1,lottery_week_str2 from numbers where lottery_time between 6925 and 6929;
select lottery_rokuyo_int,lottery_rokuyo_str from numbers where lottery_time between 6925 and 6929;
select num3_str,num3_int,num3_place1,num3_place10,num3_place100 from numbers where lottery_time between 6925 and 6929;
select num4_str,num4_int,num4_place1,num4_place10,num4_place100,num4_place1000 from numbers where lottery_time between 6925 and 6929;





C菜

元データと比較してみますね~

カレンダーを使って「曜日」や「六曜」についても検証してみましたけど、全く問題ありませんでした~

B美

さて、さっき使ったデータは第6929回目までの分なんだけど、追記する形で第6939回目までのExcelファイルがあるの

これを読み込ませる場合、どうする?

A子

さっきのコードのままだと、この全て(6939件)のレコードを追加してしまうことになるのかな?
(全部で13,868件になっちゃう)

うむむ
…ってことは、すでに登録済みの分(6929件目まで)については、データベースへの登録処理をスキップしないとダメか…

C菜

「実施回」の最大値でしたら、このSQL文で調べられますよね~

select max(lottery_time) from numbers;

A子

CakePHP5だと、どうなるの?

B美

CakePHP5の機能を使って集合関数(この場合「max」)を利用するときって、割と面倒くさいコードを書かなきゃいけないのよね

なので、今回は直接SQL文を実行するコードを書こうと思います

A子

多分、手抜きね

C菜

手抜きです~

B美

うるさいわい!
直接SQL文を実行するほうが楽なのよ

てか、ちゃんと覚えてるでしょうね?

A子

かなり昔の話だよね?

データベース操作を学んだときに出てきたような(汗)
(【CakePHP5基礎編⑦】を参照)

C菜

こうですよ~

use Cake\Datasource\ConnectionManager;

・・・

$connection = ConnectionManager::get('default');
$sql = "select max(lottery_time) from numbers";
$results = $connection->execute($sql)->fetchAll('assoc');

B美

あ、ちょっと一言…
「fetchAll()」ではなく、「fetchColumn()」のほうが良いかもね

$connection = ConnectionManager::get('default');
$time_max = $connection->execute('select max(lottery_time) from numbers')->fetchColumn(0);

これによって変数「$time_max」には項目「lottery_time」の最大値が入るんだけど、もしも空のテーブル(ゼロレコード)だったら「null」になるから気を付けてね

A子

んじゃ、スキップ処理の箇所はこうだね

if ($time_max != null && $lottery_time <= $time_max) continue;




C菜

実行してみますね~

きちんと最後の10件だけ追加されました~



B美

うん、良いんじゃない?

あ、そうだ
リファクタリングの一環なんだけどさ

今回のようにテーブルの項目数が多い場合、違う書き方があるの

C菜

リファクタリングという言葉は以前に出てきましたね~
(【CakePHP5実用編⑬】を参照)

A子

動作を変えずに、コードの見栄えを良くする…だっけ?

B美

そう

$numbers = $this->Numbers->newEmptyEntity();
$numbers->lottery_time = $lottery_time;
$numbers->lottery_date_dt = $lottery_date_dt;
$numbers->lottery_date_str = $lottery_date_str;
・・・

ではなく

$numbers = $this->Numbers->newEmptyEntity();
$numbersData = [
    'lottery_time' => $lottery_time,
    'lottery_date_dt' => $lottery_date_dt,
    'lottery_date_str' => $lottery_date_str,
    ・・・
];
$this->Numbers->patchEntity($numbers, $numbersData);

…って感じで連想配列にデータを格納してから、モデル側へ一気に渡す

A子

うーん、一応やってみるかぁ

//データベース登録
$numbers = $this->Numbers->newEmptyEntity();
$numbersData = [
    'lottery_time' => $lottery_time,
    'lottery_date_dt' => $lottery_date_dt,
    'lottery_date_str' => $lottery_date_str,
    'lottery_date_year' => $lottery_date_year,
    'lottery_date_month' => $lottery_date_month,
    'lottery_date_day' => $lottery_date_day,
    'lottery_week_int' => $lottery_week_int,
    'lottery_week_str1' => $lottery_week_str1,
    'lottery_week_str2' => $lottery_week_str2,
    'lottery_rokuyo_int' => $lottery_rokuyo_int,
    'lottery_rokuyo_str' => $lottery_rokuyo_str,
    'num3_str' => $num3_str,
    'num3_int' => $num3_int,
    'num3_place1' => $num3_place1,
    'num3_place10' => $num3_place10,
    'num3_place100' => $num3_place100,
    'num4_str' => $num4_str,
    'num4_int' => $num4_int,
    'num4_place1' => $num4_place1,
    'num4_place10' => $num4_place10,
    'num4_place100' => $num4_place100,
    'num4_place1000' => $num4_place1000,
];
$this->Numbers->patchEntity($numbers, $numbersData);

かなり面倒くさかった…(苦笑)

C菜

でも、「$numbers->○○ = $△△;」が頻繁に出現するよりは、少しスッキリしてるような気がするです~

B美

うーん、A子の書いたコードだけど、ちょっと惜しい点があります

実は「patchEntity」メソッドって、格納する値の妥当性チェックバリデーション(データ型が合ってるか…等)も(勝手に)行ってるの
んで、変数「$lottery_date_dt」の中身って「'2026-01-01'」って感じになってるわよね

これって「datetime」型じゃなくて、(おそらくは)「date」型と評価されちゃうのよ
(だって時刻が入ってないし…)

ちなみに、以前のやり方のときにうまくいっていたのは、時刻を(これまた勝手に)補完してくれてたから…(苦笑)

A子

えっと、どうすりゃ良いのさ?

B美

まずは以下のuse文を追加して…

use Cake\I18n\DateTime;

連想配列に値を格納する際、こんな感じでdatetime型にしてあげれば良いわ

'lottery_date_dt' => new DateTime($lottery_date_dt.' 00:00:00'),

A子

OK、OK

//データベース登録
$numbers = $this->Numbers->newEmptyEntity();
$numbersData = [
    'lottery_time' => $lottery_time,
    'lottery_date_dt' => new DateTime($lottery_date_dt.' 00:00:00'),
    'lottery_date_str' => $lottery_date_str,
    'lottery_date_year' => $lottery_date_year,
    'lottery_date_month' => $lottery_date_month,
    'lottery_date_day' => $lottery_date_day,
    'lottery_week_int' => $lottery_week_int,
    'lottery_week_str1' => $lottery_week_str1,
    'lottery_week_str2' => $lottery_week_str2,
    'lottery_rokuyo_int' => $lottery_rokuyo_int,
    'lottery_rokuyo_str' => $lottery_rokuyo_str,
    'num3_str' => $num3_str,
    'num3_int' => $num3_int,
    'num3_place1' => $num3_place1,
    'num3_place10' => $num3_place10,
    'num3_place100' => $num3_place100,
    'num4_str' => $num4_str,
    'num4_int' => $num4_int,
    'num4_place1' => $num4_place1,
    'num4_place10' => $num4_place10,
    'num4_place100' => $num4_place100,
    'num4_place1000' => $num4_place1000,
];
$this->Numbers->patchEntity($numbers, $numbersData);

ぱぱっと修正したよ


B美

よし、それじゃ、さっきインポートしたExcelファイルデータを全て削除してから、もう一度テスト実行してみなさい

A子

delete from numbers;」で良いかな?

B美

本番環境では「drop database numbersdb;」のほうが良いでしょうけど、開発環境だからそれでOKよ
(主キーである「id」の値が大きくなっちゃうけど…)

C菜

やってみますね~

まずは「ナンバーズ3&4_1-6929.xlsx」というExcelファイルからです~



A子

よしっ、完璧!

次は、さっきのファイルに10件追記された「ナンバーズ3&4_1-6939.xlsx」だね



C菜

データベース内の値についてもSQL文をたたいて確認してみましたけど、全然問題なかったですよ~

B美

さて、これで管理ページについては、一応完成ね

次回はいよいよ本命のWebAPI設計に入りましょう

A子

やっとかよ(苦笑)

C菜

楽しみです~