EventGrid TriggerのAzure Functionをローカルで動かす方法

Azure Function のトリガーの種類により、ローカルでの実行方法が異なります。

  • HTTP トリガー

    • http://localhost:<PORT>/api/<FUNCTION_NAME> にリクエストする事で実行できます。
  • (EventGrid Trigger を除く) HTTP トリガー以外

  • EventGrid トリガー

    • EventGrid Trigger の URL (= http://localhost:7071/runtime/webhooks/eventgrid?functionName={FUNCTION_NAME}) にリクエストする事で実行できます。

ドキュメントはこちら
Core Tools を使用してローカルで Azure Functions を開発する - ローカル関数を実行する

EventGrid TriggerのAzure Functionをローカルで動かす方法 を以下に記載します。

バージョン

  • Azure Function Runtime Version: 4.25.3.21264
  • Azure Functions Core Tools: 4.0.5413 Commit hash: N/A (64-bit)

今回やった事

  1. Azure Functions extension for Visual Studio CodeEventGrid トリガーの Azure Function を作成
  2. Event Grid Viewerアプリ で EventGrid のイベントメッセージを取得
    • EventGrid Trigger の URL にリクエストする際、 Request Body でイベントメッセージを指定する必要がある為、予め実際のイベントメッセージを取得しておきます
  3. Azure Functions Core Toolsfunc コマンド で Azure Function をローカルでホスト
  4. EventGrid Trigger の URL にリクエストし、ローカルでホストしている Azure Function をトリガー

1. Azure Functions extension for Visual Studio CodeEventGrid トリガーの Azure Function を作成

今回は Azure Functions extension for Visual Studio Code を利用して Azure Function を作成します。

ドキュメント

手順

  1. VSCode > Azure Functions extension > WORCSPACE の Function アイコンより、Create New Project... を押下

  2. TypeScript, Model V4, Azure Event Grid Trigger と選択し、関数名を入力(今回は eventGridTriggerSample)し、Enterを押下すると、eventGridTriggerSample 関数 が作成される

2. Event Grid Viewer アプリ で EventGrid のイベントメッセージを取得

EventGrid Trigger の URL にリクエストする際、 Request Body でイベントメッセージを指定する必要がある為、予め実際のイベントメッセージを取得しておきます。

今回は Azure BLOB Storage Container へファイルがアップロードされた際のイベントメッセージを、Event Grid Viewerアプリで取得します 。

2-1. Event Grid Viewer アプリをデプロイ

Event Grid Viewer アプリ = ドキュメントの ビューアー Web アプリ の事。

EventGridのイベントメッセージをキャプチャする為の Event Grid Viewer アプリ を、learn.microsoft.com にあるARMテンプレートを利用して Azure AppService にカスタム デプロイします。

ドキュメント

手順

  1. learn.microsoft.com の ビューアー Web アプリを作成するより、Deploy to Azure ボタンを押下

  2. ブラウザで https://portal.azure.com/#create/Microsoft.Template が起動するので、サブスクリプション/リソースグループと、インスタンスの詳細を入力

  3. デプロイが成功すると以下のリソースが作成される

2-2. Event Grid の イベント サブスクリプション を作成

ドキュメント

手順

  1. ファイルのアップロード先となる Azure BLOB Storage Container を作成

    • ストレージアカウント を作成
    • ストレージアカウントデータストレージ: コンテナー (コンテナ名: upload) を作成
  2. Event Grid の イベント サブスクリプション を作成 (※1)

    • サブスクリプションEvent Grid リソース プロバイダー を登録
    • Event Grid システム トピック 及び イベント サブスクリプション を作成
      • イベント サブスクリプション > 「+ イベントブスクリプション」 より以下の内容で作成
        • トピックの種類
          • ストレージアカウント
        • ソースリソース
          • 上記で作成したストレージアカウント
        • エンドポイントのタイプ
          • webhook
        • エンドポイント ※イベントの送信先
          • https://{「Event Grid Viewer アプリ」のドメイン}/api/updates
            • 今回の例: https://viewerwebapptest1698469260.azurewebsites.net/api/updates

※1 ハマりどころ

Azure Portalのバグなのか Event Grid トピック > (対象のトピックを選択) > 「+ イベントブスクリプション」 からだとサブスクリプションが作成できなかった

2-3. Event Grid Viewer アプリ で EventGrid のイベントメッセージをキャプチャ

手順

  1. ブラウザで Event Grid Viewer アプリ を開いておく

  2. 上記で作成した Azure BLOB Storage Container にファイルをアップロード

  3. Event Grid Viewer アプリEvent Grid イベント メッセージ が届くので、Web上でメッセージ内容を確認

{
  "topic": "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260",
  "subject": "/blobServices/default/containers/upload/blobs/2023/10/28/test.txt",
  "eventType": "Microsoft.Storage.BlobCreated",
  "id": "1afcef29-f01e-0079-4e61-09740a066060",
  "data": {
    "api": "PutBlob",
    "clientRequestId": "50bdeec1-31cb-4fa4-b783-ea6fbb26400d",
    "requestId": "1afcef29-f01e-0079-4e61-09740a000000",
    "eTag": "0x8DBD778B179D29D",
    "contentType": "text/plain",
    "contentLength": 4,
    "blobType": "BlockBlob",
    "url": "https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt",
    "sequencer": "0000000000000000000000000000EC5E000000000009cc7f",
    "storageDiagnostics": {
      "batchId": "3875e5f8-9006-0022-0061-094d31000000"
    }
  },
  "dataVersion": "",
  "metadataVersion": "1",
  "eventTime": "2023-10-28T05:42:37.6655517Z"
}

3. Azure Functions Core Toolsfunc コマンド で Azure Function をローカルでホスト

Azure Functions Core Toolsfunc コマンド で Azure Function をローカルで起動します

ドキュメント

手順

  • Azurite コンテナを起動

    • docker run -p 10000:10000 -p 10001:10001 mcr.microsoft.com/azure-storage/azurite
  • プロジェクトの直下に local.settings.json を以下の内容で設置

    • "AzureWebJobsStorage": "UseDevelopmentStorage=true" は azurite を使用する設定
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
  }
}
  • tsc で TypeScript を JavaScript に変換

    • yarn build
  • funcコマンド で eventGridTriggerSample 関数をローカルで起動

    • func start eventGridTriggerSample --typescript
$ func start eventGridTriggerSample --typescript

Azure Functions Core Tools
Core Tools Version:       4.0.5413 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.25.3.21264

[2023-10-28T06:06:17.001Z] Worker process started and initialized.

Functions:

    eventGridTriggerSample: eventGridTrigger

For detailed output, run func with --verbose flag.
[2023-10-28T06:06:22.030Z] Host lock lease acquired by instance ID '000000000000000000000000816E9EDC'.

EventGrid Trigger の URL にリクエストし、ローカルでホストしている Azure Function をトリガー

EventGrid Trigger の URL を呼び出すことで EventGrid Trigger の Azure Function をローカルで実行する事ができます。

ドキュメント

EventGrid Trigger の URL

  • http://localhost:7071/runtime/webhooks/eventgrid?functionName={FUNCTION_NAME}

手順

  1. EventGrid Trigger の URL にリクエス
    • Request Body で上記で取得した 「EventGried の イベントメッセージ」を指定して POST
curl http://localhost:7071/runtime/webhooks/eventgrid?functionName=eventGridTriggerSample \
-H "Content-Type: application/json" \
-H "aeg-event-type: Notification" \
-d '{
  "topic": "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260",
  "subject": "/blobServices/default/containers/upload/blobs/2023/10/28/test.txt",
  "eventType": "Microsoft.Storage.BlobCreated",
  "id": "1afcef29-f01e-0079-4e61-09740a066060",
  "data": {
    "api": "PutBlob",
    "clientRequestId": "50bdeec1-31cb-4fa4-b783-ea6fbb26400d",
    "requestId": "1afcef29-f01e-0079-4e61-09740a000000",
    "eTag": "0x8DBD778B179D29D",
    "contentType": "text/plain",
    "contentLength": 4,
    "blobType": "BlockBlob",
    "url": "https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt",
    "sequencer": "0000000000000000000000000000EC5E000000000009cc7f",
    "storageDiagnostics": {
      "batchId": "3875e5f8-9006-0022-0061-094d31000000"
    }
  },
  "dataVersion": "",
  "metadataVersion": "1",
  "eventTime": "2023-10-28T05:42:37.6655517Z"
}' --verbose

作業ログ: EventGrid Trigger の URL にリクエストし、eventGridTriggerSample 関数をトリガーした様子

$ curl http://localhost:7071/runtime/webhooks/eventgrid?functionName=eventGridTriggerSample \
> -H "Content-Type: application/json" \
> -H "aeg-event-type: Notification" \
> -d '{
>   "topic": "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260",
>   "subject": "/blobServices/default/containers/upload/blobs/2023/10/28/test.txt",
>   "eventType": "Microsoft.Storage.BlobCreated",
>   "id": "1afcef29-f01e-0079-4e61-09740a066060",
>   "data": {
>     "api": "PutBlob",
>     "clientRequestId": "50bdeec1-31cb-4fa4-b783-ea6fbb26400d",
>     "requestId": "1afcef29-f01e-0079-4e61-09740a000000",
>     "eTag": "0x8DBD778B179D29D",
>     "contentType": "text/plain",
>     "contentLength": 4,
>     "blobType": "BlockBlob",
>     "url": "https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt",
>     "sequencer": "0000000000000000000000000000EC5E000000000009cc7f",
>     "storageDiagnostics": {
>       "batchId": "3875e5f8-9006-0022-0061-094d31000000"
>     }
>   },
>   "dataVersion": "",
>   "metadataVersion": "1",
>   "eventTime": "2023-10-28T05:42:37.6655517Z"
> }' --verbose
*   Trying 127.0.0.1:7071...
* Connected to localhost (127.0.0.1) port 7071 (#0)
> POST /runtime/webhooks/eventgrid?functionName=eventGridTriggerSample HTTP/1.1
> Host: localhost:7071
> User-Agent: curl/8.1.2
> Accept: */*
> Content-Type: application/json
> aeg-event-type: Notification
> Content-Length: 983
>
< HTTP/1.1 202 Accepted
< Content-Length: 0
< Date: Sat, 28 Oct 2023 06:28:51 GMT
< Server: Kestrel
<
* Connection #0 to host localhost left intact
~ $
  • eventGridTriggerSample 関数がトリガーされた
[2023-10-28T06:28:51.995Z] Executing 'Functions.eventGridTriggerSample' (Reason='EventGrid trigger fired at 2023-10-28T15:28:51.9435940+09:00', Id=ca807f6d-683c-4ddd-a68c-4e088fa4e650)
[2023-10-28T06:28:52.081Z] Event grid function processed event: {
[2023-10-28T06:28:52.081Z]   topic: '/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260',
[2023-10-28T06:28:52.081Z]   subject: '/blobServices/default/containers/upload/blobs/2023/10/28/test.txt',
[2023-10-28T06:28:52.081Z]   eventType: 'Microsoft.Storage.BlobCreated',
[2023-10-28T06:28:52.081Z]   id: '1afcef29-f01e-0079-4e61-09740a066060',
[2023-10-28T06:28:52.081Z]   data: {
[2023-10-28T06:28:52.081Z]     api: 'PutBlob',
[2023-10-28T06:28:52.081Z]     clientRequestId: '50bdeec1-31cb-4fa4-b783-ea6fbb26400d',
[2023-10-28T06:28:52.081Z]     requestId: '1afcef29-f01e-0079-4e61-09740a000000',
[2023-10-28T06:28:52.081Z]     eTag: '0x8DBD778B179D29D',
[2023-10-28T06:28:52.081Z]     contentType: 'text/plain',
[2023-10-28T06:28:52.081Z]     contentLength: 4,
[2023-10-28T06:28:52.081Z]     blobType: 'BlockBlob',
[2023-10-28T06:28:52.081Z]     url: 'https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt',
[2023-10-28T06:28:52.081Z]     sequencer: '0000000000000000000000000000EC5E000000000009cc7f',
[2023-10-28T06:28:52.081Z]     storageDiagnostics: { batchId: '3875e5f8-9006-0022-0061-094d31000000' }
[2023-10-28T06:28:52.081Z]   },
[2023-10-28T06:28:52.081Z]   dataVersion: '',
[2023-10-28T06:28:52.081Z]   metadataVersion: '1',
[2023-10-28T06:28:52.081Z]   eventTime: '2023-10-28T05:42:37.6655517Z'
[2023-10-28T06:28:52.081Z] }
[2023-10-28T06:28:52.112Z] Executed 'Functions.eventGridTriggerSample' (Succeeded, Id=ca807f6d-683c-4ddd-a68c-4e088fa4e650, Duration=155ms)

サンプル

CakePHP 4.x に ParaTest を導入

CakePHP 4.x に ParaTest を導入

テストの数が増えてきて全件テストを実行するのに時間がかかるようになった為、ParaTestを導入し、テストを並列実行するようにしてみました。

version

ドキュメント

ParaTest

  • PHPUnit でのテストを並列で実行できるツール
    • 並列数(プロセス数)はオプションで指定できる
    • テストクラス単位で並行実行される模様
  • 環境変数 ParaTest に並列実行されるプロセスごとに1から始まる連番が設定される
    • DB を利用したテストを並列実行する際は、TEST_TOKEN を利用して、各プロセス毎に利用するDBを分けるが必要がある
  • Laravel 10.x の並列テストでは内部的に ParaTest を使用

導入効果

元々 06:27 かかっていたところが 01:48 で実行できるようになりました

  • ParaTest (8 プロセス) : 01:48
  • PHPUnit (1 プロセス) : 06:27

CakePHP 4.x への導入方法のメモ

DB が絡むテストを並列実行する際は、TEST_TOKEN を利用して、各プロセス毎に利用するDBを分ける等の工夫が必要になる

  • 今回はCakePHPのコアのコードを読み、以下のようにする事で、各プロセス毎に利用するDBの出し分けを実現しました
    • Fixtureの調整あたりで少しハマりました

ParaTest をインストール

composer require --dev brianium/paratest --with-all-dependencies

並列数分のテスト用DBを作成する

  • 今回は 8 プロセスで並列実行する想定なので、並列数分のテスト用DBを作成
  • 作成方法は割愛

app_local.phpDatasources に ParaTest 用の connection の設定を追加

// app/config/app_local.php

$paraTestConnectionBase = [
    'className' => Connection::class,
    'driver' => //...,
    //...

    'host' => env('DB_HOST'),
    'port' => env('DB_PORT'),
    'username' => env('DB_USER'),
    'password' => env('DB_PASSWORD'),
    'encoding' => env('DB_ENCODING', 'UTF8'),
    'timezone' => env('DB_TIMEZONE', 'UTC'),
]

// ...

return [
    //...
    'Datasources' => [
        // ...

        // ParaTest用のconnection
        // note: connection名を `test_` で始まるものにする必要がある
        'test_1' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test1'],
        'test_2' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test2'],
        'test_3' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test3'],
        'test_4' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test4'],
        'test_5' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test5'],
        'test_6' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test6'],
        'test_7' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test7'],
        'test_8' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test8'],

TEST_TOKEN を元に、各プロセス毎の Connection の設定を調整

  • \Cake\TestSuite\Fixture\PHPUnitExtension::executeBeforeFirstTest の処理をカスタマイズする必要があった為、PHPUnitBeforeFirstTestHook を利用し、以下のようにカスタマイズ
// app/phpunit.xml.dist

//...
  <extensions>
    <extension class="Path\To\AppExtension"/>
    //...
// Path/To/AppExtension.php

//...
use PHPUnit\Runner\BeforeFirstTestHook;

//...
class AppExtension implements BeforeFirstTestHook
{
    /**
     * what:
     *  \Cake\TestSuite\Fixture\PHPUnitExtension::executeBeforeFirstTest の処理に、
     *  ParaTest 使用時の connection を調整する為の処理を追加したもの
     */
    public function executeBeforeFirstTest(): void
    {
        $helper = new ConnectionHelper();
        $helper->addTestAliases();

        // for ParaTest
        $paraTestToken = getenv('TEST_TOKEN');
        if ($paraTestToken !== false) {
            // ParaTestで各プロセス用のconnectionを設定する為に、
            // \Cake\TestSuite\ConnectionHelper::addTestAliases で設定した ConnectionManager::alias('test', 'default'); を上書き
            ConnectionManager::alias("test_{$paraTestToken}", 'default');
        }

        $enableLogging = in_array('--debug', $_SERVER['argv'] ?? [], true);
        if ($enableLogging) {
            $helper->enableQueryLogging();
            Log::drop('queries');
            Log::setConfig('queries', [
                'className' => 'Console',
                'stream' => 'php://stderr',
                'scopes' => ['queriesLog'],
            ]);
        }
    }

Fixture で利用する connection を調整

  • TEST_TOKEN を元に、当該プロセス用のconnectionを設定
// app/tests/Fixture/AppTestFixture.php

//...
use Cake\TestSuite\Fixture\TestFixture;

//...
class AppTestFixture extends TestFixture
{
    public function __construct()
    {
        // ParaTestの場合は、当該プロセス用のconnectionを設定
        $paraTestToken = getenv('TEST_TOKEN');
        if ($paraTestToken !== false) {
            $this->connection = "test_{$paraTestToken}";
        }

        parent::__construct();
    }
}
class XxxxFixture extends AppTestFixture

Test用DBのmigration方法を調整

  • こちらも TEST_TOKEN を元に当該プロセスで利用するDBをmigrateするように調整
  • 各DB毎に1回だけmigrateされるように調整
// app/tests/bootstrap.php

// Run migrations for multiple plugins
$migrator = new Migrator();

$paraTestToken = getenv('TEST_TOKEN');

// for ParaTest
if ($paraTestToken !== false) {
    $dirName = TMP . 'tests' . DS . 'paratest' . DS;
    $fileName = "migrated-test_{$paraTestToken}";
    $path = $dirName . $fileName;

    // 1DBにつき1回のみmigrate
    if (!file_exists($path)) {
        $DBName = "test_{$paraTestToken}";
        $connectionName = $DBName;

        $migrator->runMany([
            //...
            ['connection' => $connectionName, 'plugin' => '//...'],
        ]);

        if (!file_exists($dirName)) {
            mkdir($dirName);
        }
        touch($path);
    }
} else {
    $migrator->runMany([
        //...
        ['connection' => 'test', 'plugin' => '//...'],
    ]);
}

上記で作成した一時ファイルをParaTest実行前に削除するように設定

// composer.json

{
    //..
    "scripts": {
        //..
        "paratest": "rm -rf tmp/tests/paratest && ./vendor/bin/paratest -p 8",

ParaTest実行

composer paratest

CakePHP 4.x: Entityのカラム一覧は、DBのスキーマを参照して取得しており、取得結果は schema_cache としてキャッシュされている

CakePHP の実装

  • 実際のスキーマを元に、Entity 取得時に実行される SQL の SELECT 句に指定するカラム一覧を生成している
  • 上記は schema_cache としてキャッシュされる
    • bin/cake schema_cache clear した上で、Entity を取得した際、カラムの一覧を取得する SQL が実行される事を確認
    • bin/cake schema_cache clear せず、Entity を取得した際、カラムの一覧を取得する SQL が実行されない事を確認
  • migration ファイルで \Migrations\Table::update を実行時に schema_cache が再考構築される
    • これにより、通常は、カラム削除を伴う migration を実行しても問題が起こらない

ハマった事

  • BGデプロイメントで、カラム削除を伴う migration をデプロイした際に、ブルー側で、存在しないカラムを SELECT する SQL が実行されエラーになった
  • why
    • グリーン側で migration を行った後、ブルー側で bin/cake schema_cache clear していなかった為、schema_cache がクリアされず、CakePHP内部のカラム一覧と実際のカラム一覧に差異が生じた
    • 差異により、存在しないカラムを SELECT する SQL が実行された
  • 対策
    • 対象 Table クラスのコンストラクタで $this->getSchema()->removeColumn('対象カラム名') を実行する事で、schema_cache が古くても、「存在しないカラムの SELECT」を回避できる
    • migration のデプロイ前に、予め $this->getSchema()->removeColumn('対象カラム名') をデプロイしておく事で今回のエラーを回避できる
// 対象テーブルクラス

public function __construct(array $config = [])
{
    parent::__construct($config);

    // これにより、 `SELECT 削除予定のカラム, ...` が発生しなくなる
    $this->getSchema()->removeColumn('削除予定のカラム名');
}

ドキュメント

アプリケーションをデプロイする時にプラグインを使用する場合、 テーブルのカラムメタデータを更新するように、必ず ORM キャッシュをクリアしてください。 そうしなければ、それらの新しいカラムの操作を実行する時に、カラムが存在しないエラーになります。

M2 Mac で Apollo Twin X を使えるようにする方法

2023/02/11 の時点では、macmacOS 復旧 で起動し、“低セキュリティ ”オプション を設定する必要があります。

作業日

  • 2023/02/11

経緯

  • mac book pro 2016 (13 インチ) が突然故障し、再起不能となり、M2 Max の mac book pro (14インチ) を購入
  • 旧PCは起動できない状態
  • 新PCに、Time Machine をバックアップを復元した

備考

  • UADハードウェアのトランスファー が必要になる事を懸念していたが、トランスファーは必要なかった

Apollo Twin X を使えるようにする方法 (作業ログ)

  • まず、UAD Meter & Control Panel を再インストール
    1. こちら を参考にアンインストール
    2. https://www.uaudio.jp/downloads/uadmacOS用をダウンロード より UAD-v10.2.2-Mac.zip をダウンロードし、解答した中身の UAD-v10.2.2-Mac.pkg を実行し、再インストール  
  • UAD Meter & Control Panel に表示されるエラーに応じて、下記を実施

    • Driver not installed correctly が表示される場合

    • Connect UA Device が表示される場合

      • Apollo Twin Xmac に正しく接続されている事を確認 (ケーブルが抜けていてハマりました...)
    • Click Allow in Security & Privacy が表示される場合

      • システム設定 > プライバシーとセキュリティ > セキュリティ から 開発元"Universa Audio"のシステムソフトウェアがアップデートされました。許可 を押下
        • Ventura でシステム設定のUIが変更されているので、参考サイトを読む際は、スクショが古い事を念頭に置いて参考サイトを読む
  • 効果があったかどうかわからないこと

TODO

  • UAD Meter & Control Panel が正式に M2 Mac に対応したら、 “低セキュリティ ”オプション を解除すること!

教訓

  • Time Machine のバックアップを常時すること
    • 3 週間ほどバックアップをサボってしまい、その間に作成した譜面や、リハ/ライブの音源を消失しました...

PHPStorm で File Structure のショートカット(cmd + F12)が効かない事象の調査メモ

事象

  • cmd + F8(Breakpoints) などは効く
  • cmd + F12 だけ効かない
  • キーストロークは IDE に到達しますか?
    • => 到達しないので、他のプログラムまたはオペレーティングシステムがショートカットをインターセプト している

解消方法

今回のログ

https://twitter.com/idubmorgan/status/1545529993282797568

Azure DevOps Pipelines: pr: none を明示しないと、PR作成時に、意図せず Pipeline が動いてしまう

  • デフォルトで生成される azure-pipelines.yml は以下
    • 注意点
      • pr: none が明示されていなので、PR作成時に Pipeline が動く
trigger:
- main

pool:
  vmImage: ubuntu-latest
steps:
- script: echo Hello, world!
  displayName: 'Run a one-line script'
- script: |
    echo Add other tasks to build, test, and deploy your project.
    echo See https://aka.ms/yaml
  displayName: 'Run a multi-line script'

検証用リポジトリ

https://github.com/tamurayk/azure_devops_pipeline_test

CakePHP4.x: hasMany の Association を contain すると 別SQL になる仕組み

version

仕組み

// 流れ
\Cake\Database\Driver::prepare
  \Cake\ORM\Query::sql
    \Cake\ORM\Query::_transformQuery
      \Cake\ORM\EagerLoader::attachAssociations
        \Cake\ORM\EagerLoader::attachableAssociations
          \Cake\ORM\EagerLoader::_resolveJoins

理解できていない事

  • hasMany の Association を contain した場合は、別SQLにする、という設計になっている理由 (設計思想)

作業ログ

以下のような Learning Test を書いて、デバッグした

    /**
     * @group leaning
     */
    public function testHasMany(): void
    {
      //..

      $query = $ArticlesTable->find();

      // Articles belongsTo Authors
      // Articles hasMany Comments
      $query->contain(['Authors', 'Comments']);
      $result_set = $query->all();

      // ...
    }

デバッグログ

// Leaning Test 内で \Cake\ORM\Query::all を実行
string(17) "get_class($query)"
string(14) "Cake\ORM\Query"
string(54) "↓ \Cake\ORM\Query::all $this->getRepository()::class"
  string(28) "App\Model\Table\ArticlesTable"
  string(69) "↓↓ \Cake\Datasource\QueryTrait::all $this->getRepository()::class"
    string(28) "App\Model\Table\ArticlesTable"
      string(65) "↓↓↓ \Cake\ORM\Query::_execute $this->getRepository()::class"
      string(28) "App\Model\Table\ArticlesTable"
      string(121) "!!!!!!!! \Cake\ORM\Query::_execute | $statement = $this->getEagerLoader()->loadExternal($this, $this->execute()) !!!!!!!!"
        string(72) "↓↓↓↓ \Cake\Database\Query::execute $this->getRepository()::class"
          string(28) "App\Model\Table\ArticlesTable"
          string(30) "\Cake\Database\Connection::run"
          string(34) "\Cake\Core\Retry\CommandRetry::run"
          string(43) "\Cake\Database\Connection::run の callback"
          string(34) "\Cake\Database\Connection::prepare"
          string(34) "\Cake\Core\Retry\CommandRetry::run"
          string(47) "\Cake\Database\Connection::prepare の callback"
          string(34) "\Cake\Database\Driver::prepare (1)"
          string(38) "\Cake\Database\Driver::prepare (2) sql"
          string(1134) "SELECT ... FROM Articles Articles INNER JOIN Authors Authors ON (Authors.id = (Articles.author_id))"
          string(60) "\Cake\Database\Driver::prepare (end) return new PDOStatement"
        string(121) "↑↑↑↑ \Cake\Database\Query::execute (end) return \Cake\Database\StatementInterface | $this->getRepository()::class"
        string(28) "App\Model\Table\ArticlesTable"
        string(83) "↓↓↓↓↓ \Cake\ORM\EagerLoader::loadExternal  $query->getRepository()::class"
          string(28) "App\Model\Table\ArticlesTable"
          string(81) "\Cake\ORM\EagerLoader::loadExternal (2) $external is not empty | count($external)"
          int(1)
          string(68) "\Cake\ORM\EagerLoader::loadExternal (3) foreach ($external as $meta)"
          string(62) "\Cake\ORM\EagerLoader::loadExternal (3-1) ※foreach内 $alias"
          string(7) "Articles"
          string(54) "↓ \Cake\ORM\Query::all $this->getRepository()::class"
            string(26) "App\Model\Table\CommentsTable"
            string(69) "↓↓ \Cake\Datasource\QueryTrait::all $this->getRepository()::class"
              string(26) "App\Model\Table\CommentsTable"
              string(65) "↓↓↓ \Cake\ORM\Query::_execute $this->getRepository()::class"
                string(26) "App\Model\Table\CommentsTable"
                string(121) "!!!!!!!! \Cake\ORM\Query::_execute | $statement = $this->getEagerLoader()->loadExternal($this, $this->execute()) !!!!!!!!"
                  string(72) "↓↓↓↓ \Cake\Database\Query::execute $this->getRepository()::class"
                    string(26) "App\Model\Table\CommentsTable"
                    string(30) "\Cake\Database\Connection::run"
                    string(34) "\Cake\Core\Retry\CommandRetry::run"
                    string(43) "\Cake\Database\Connection::run の callback"
                    string(34) "\Cake\Database\Connection::prepare"
                    string(34) "\Cake\Core\Retry\CommandRetry::run"
                    string(47) "\Cake\Database\Connection::prepare の callback"
                    string(34) "\Cake\Database\Driver::prepare (1)"
                    string(38) "\Cake\Database\Driver::prepare (2) sql"
                    string(399) "SELECT ... FROM Comments Comments WHERE Comments.article_id in (:c0)"
                    string(60) "\Cake\Database\Driver::prepare (end) return new PDOStatement"
                  string(121) "↑↑↑↑ \Cake\Database\Query::execute (end) return \Cake\Database\StatementInterface | $this->getRepository()::class"
                  string(26) "App\Model\Table\CommentsTable"
                  string(83) "↓↓↓↓↓ \Cake\ORM\EagerLoader::loadExternal  $query->getRepository()::class"
                    string(26) "App\Model\Table\CommentsTable"
                  string(78) "↑↑↑↑↑ \Cake\ORM\EagerLoader::loadExternal (1-end) $external is empty"
              string(113) "↑↑↑ \Cake\ORM\Query::_execute (end) return new ResultSet($this, $statement) | $this->getRepository()::class"
              string(26) "App\Model\Table\CommentsTable"
            string(120) "↑↑ \Cake\Datasource\QueryTrait::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class"
            string(26) "App\Model\Table\CommentsTable"
            string(106) "↑ \Cake\ORM\Query::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class "
          string(26) "App\Model\Table\CommentsTable"
          string(126) "\Cake\ORM\EagerLoader::loadExternal (3-end) ※foreach内 $statement = new CallbackStatement($statement, $driver, $f) | $alias"
          string(7) "Articles"
        string(98) "↑↑↑↑↑ \Cake\ORM\EagerLoader::loadExternal (end) return \Cake\Database\StatementInterface"
      string(113) "↑↑↑ \Cake\ORM\Query::_execute (end) return new ResultSet($this, $statement) | $this->getRepository()::class"
    string(28) "App\Model\Table\ArticlesTable"
  string(120) "↑↑ \Cake\Datasource\QueryTrait::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class"
  string(28) "App\Model\Table\ArticlesTable"
string(106) "↑ \Cake\ORM\Query::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class "
string(28) "App\Model\Table\ArticlesTable"

今後の学習テーマ

  • hasManyのTableをSearchプラグインで検索する際の問題点と対応

  • note

    • contain すると 別SQL になる為、beforeProcessでJOINする必要がある
    • 意図した検索結果を得るには ID で DISTINCTする必要がある
    • ID 以外で ORDER BY したい
      • DISTINCT で指定したカラムでしか ORDER BY できない