## モジュール 5: オンラインスキーマ変更

TiDB は、オンライン DDL を用いることでオンラインスキーマ変更を実現できます。これにより、テーブルの定義（カラムの追加・削除、カラム属性の変更、インデックスの追加・削除、テーブル名の変更など）をアプリケーションを停止せずに変更できます。これらの操作は、ゼロダウンタイムでオンライン実行可能です。原理についての詳細は [TiDB における DDL 実行のベストプラクティス](https://docs.pingcap.com/tidb/stable/ddl-introduction) を参照してください。

このモジュールでは、アプリケーションの実行中に、`SHOP`.`TRANSACTIONS` テーブルに `Discount` 列を追加します。

また、本モジュール修了後に [TiDB Cloud Starter 紹介](/labs/demo_203) の「モジュール 5: 非ブロッキングなオンライン DDL でスキーマを進化」を参照して、オンライン DDL の基本を復習することもできます。

### 手順 1. 「注文履歴」ページとデータベース内のテーブルの確認

+ [注文履歴](${LAB:HTTPS_REVERSE_PROXY_URL}?next=${LAB:"/vscode/proxy/8000/transactions"|urlencode})で「注文履歴」ページにアクセスします。このラボでは、テーブルに `割引` カラムを追加することを目指します。

![204_5_1_1.png 85%](https://lab-static.pingcap.com/images/2026/1/23/075271364913ac28e4967d9ae3b84062c3bcecb4.png)

+ VS Codeで**新しいターミナル**を開きます（以降「VS Code ターミナル 2」と呼びます）。

![204_5_1_2.png 85%](https://lab-static.pingcap.com/images/2026/1/22/d3c60da79b95bd149d9bde24611fe3996e42d374.png)

+ VS Code ターミナル2で、`php artisan model:show Transaction` コマンドを実行し、Model `APP/Model/Transaction.php` に対応するテーブル `SHOP`.`TRANSACTIONS` の定義を表示します。

  ```bash
  php artisan model:show Transaction
  ```

  ```output
  App\Models\Transaction .....................................................
  Database ............................................................. mysql
  Table ......................................................... transactions

  Attributes ..................................................... type / cast
  id unique, fillable .............................. bigint(20) unsigned / int
  user_id fillable ....................................... bigint(20) unsigned
  product_id fillable .................................... bigint(20) unsigned
  price fillable ............................................... decimal(10,2)
  buy_count fillable ................................................. int(11)
  total_price fillable ......................................... decimal(18,2)
  created_at nullable ................................... timestamp / datetime
  updated_at nullable ................................... timestamp / datetime
  pay_type fillable ............................................... bigint(20)

  Relations ..................................................................
  Product HasOne ...................................... App\Models\Product
  User HasOne ................................................ App\Models\User
  PayType HasOne .......................................... App\Models\PayType

  Events .....................................................................

  Observers ..................................................................
  ```

+ あるいは、VS Code ターミナル2で `php artisan db:table transactions` コマンドを使用し、テーブル `SHOP`.`TRANSACTIONS` の定義を確認して、 `Discount` カラムが存在しないことを確認します。

  ```bash
  php artisan db:table transactions
  ```

  ```output
  shop.transactions ..........................................................
  Columns .................................................................. 9
  Size ............................................................... 2.02 KB
  Engine .............................................................. InnoDB
  Collation ............................................... utf8mb4_unicode_ci

  Column ................................................................ Type
  id bigint .............................................. bigint(20) unsigned
  user_id bigint ......................................... bigint(20) unsigned
  product_id bigint ...................................... bigint(20) unsigned
  price decimal ................................................ decimal(10,2)
  buy_count int ...................................................... int(11)
  total_price decimal .......................................... decimal(18,2)
  created_at timestamp, nullable ................................... timestamp
  updated_at timestamp, nullable ................................... timestamp
  pay_type bigint ............................................... 1 bigint(20)

  Index ......................................................................
  primary id .................................................. btree, primary
  user_transaction_idx user_id ......................................... btree
  product_transaction_idx product_id ................................... btree
  ```

### 手順 2. オンラインスキーマ変更の準備

+ VS Code ターミナル2で、MySQLクライアントを使ってTiDB Cloudに接続します。

   ```bash
   mysql --comments -u ${LAB:SERVERLESS_CLUSTER_USERNAME} \
   -h ${LAB:SERVERLESS_CLUSTER_HOST} -P ${LAB:SERVERLESS_CLUSTER_PORT} \
   --ssl \
   --ssl-ca=/etc/ssl/certs/ca-certificates.crt \
   --ssl-verify-server-cert \
   -p${LAB:SERVERLESS_CLUSTER_PASSWORD} \
   ${LAB:SERVERLESS_CLUSTER_DATABASE_NAME}
   ```

   ```output
   Your MySQL connection id is 61317775
   Server version: 8.0.11-TiDB-v7.5.2-serverless TiDB Server (Apache License 2.0) Community Edition, MySQL 8.0 compatible

   Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

   Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

   MySQL [shop]>
   ```

+ VS Code ターミナル 2で、システム変数 `tidb_enable_metadata_lock` の値を確認するコマンドを実行します。デフォルト値は `ON` です。

  ```sql
  SHOW GLOBAL VARIABLES LIKE 'tidb_enable_metadata_lock';
  ```

  ```output
  MySQL [shop]> SHOW GLOBAL VARIABLES LIKE 'tidb_enable_metadata_lock';
  +---------------------------+-------+
  | Variable_name             | Value |
  +---------------------------+-------+
  | tidb_enable_metadata_lock | ON    |
  +---------------------------+-------+
  1 row in set (0.011 sec)
  ```

+ システム変数  `tidb_enable_metadata_lock` の値を `OFF` に変更します。VS Code で「Are you sure you want to paste 2 lines of text into the terminal?」と尋ねるウィンドウがポップアップ表示されるので、「Paste」をクリックしてください。

   ![204_5_2_1.png 85%](https://lab-static.pingcap.com/images/2026/2/6/f59a9fb8fb4b13e9f37efd228ef37c04b1da3a1f.png)

  ```sql
  SET GLOBAL tidb_enable_metadata_lock = OFF;
  SHOW GLOBAL VARIABLES LIKE 'tidb_enable_metadata_lock';
  ```

  ```output
  MySQL [shop]> SET GLOBAL tidb_enable_metadata_lock = OFF;
  Query OK, 0 rows affected (0.016 sec)

  MySQL [shop]> SHOW GLOBAL VARIABLES LIKE 'tidb_enable_metadata_lock';
  +---------------------------+-------+
  | Variable_name             | Value |
  +---------------------------+-------+
  | tidb_enable_metadata_lock | OFF   |
  +---------------------------+-------+
  1 row in set (0.011 sec)
  ```

+ 注: システム変数 `tidb_enable_metadata_lock` が `ON` （デフォルト値）の場合、すべてのDDLコマンドは関連テーブルに対するDMLステートメントが完了するのを待つ必要があります。この待機期間中、DDLコマンドはブロックされ、これはMySQLの動作と一致します。システム変数 `tidb_enable_metadata_lock` が `OFF` の場合にのみ、DDLコマンドの実行はDMLコマンドの影響を受けなくなります。詳細については、[TiDB Cloud Starter 紹介](/labs/demo_203)の「モジュール 5: ノンブロッキング・オンライン DDL でスキーマ進化」を参照してください。

+ VS Code ターミナル 2で、MySQLクライアントを終了します。

   ```sql
   EXIT
   ```

   ```output
   MySQL [shop]> EXIT
   Bye
   root@0c2c53eace8f:~/Mshop#
   ```

+ コントローラー `app/Http/Controllers/ProductsController.php` 内の関数 `public function buy(Request $request, Product $product)` を変更します。この関数は購入トランザクション機能を実装しており、スキーマ変更プロセス中に繰り返し実行される可能性があります。変数 `$OnlineSchemaChange` の初期値を `false` から `true` に変更し、ファイルを保存します。変更はすぐに反映されます。

  ```php
  /**
  * Process the products purchase.
  */
  public function buy(Request $request, Product $product)
  {
    try {
        //Validate
        $validated = $request->validate(rules: [
            'buy_count' => 'required|integer|min:1',
            'pay_with' => 'required|integer|min:1',
        ]);
    } catch(ValidationException $e) {

        return redirect('/products/' . $product->id)
            ->withErrors($validated)
            ->withInput();
    }

    /**
    * If $OnlineSchemaChange is true, execute function $this->executeTransactionWithRetry(...) to prevent from the
    * error 'Error 8028: Information schema is changed during the execution of the statement' by retry for purchase transaction.
    * If $OnlineSchemaChange is true, exectue function $this->executeTransaction(...) for purchase transaction without retry.
    */
    $OnlineSchemaChange = true;

    if($OnlineSchemaChange) {

        //Execute the logic of purchase transaction.
        return $this->executeTransactionWithRetry($request, $product, $validated);
    } else {
        //Use recursive functions to handle retry logic of purchase transaction.
        return $this->executeTransaction($request, $product, $validated);

    }
  }
  ```

+ 上記の変更の理由は、Online DDLが `SHOP`.`TRANSACTIONS` テーブルで実行を開始すると、DDLコマンドの前に開始され、現在 `SHOP`.`TRANSACTIONS` で実行中のトランザクションが、テーブル構造の変更により失敗する可能性があるためです (ERROR 8028: Information schema is changed)。そのため、アプリケーションには、再試行を通じてこの問題を解決し、すべてのトランザクションが正常に実行されるようにする別の関数 `private function executeTransactionWithRetry($request, $product, $validated, $retryCount = 0)` が導入されています。詳細については、[TiDB Cloud Starter 紹介](/labs/demo_203)の「モジュール 5: ノンブロッキング・オンライン DDL でスキーマ進化」を参照してください。

### 手順 3. アプリケーションから継続的な変更を実行する

+ [書籍管理](${LAB:HTTPS_REVERSE_PROXY_URL}?next=${LAB:"/vscode/proxy/8000/products/manage"|urlencode}) をクリックして管理ページにアクセスし、「自動変更を開始」ボタンをクリックします。この時点から、ページは自動的にランダムにトランザクションと在庫補充を実行します。分かりやすくするために、「Waterfalls」というタイトルの本のみを更新します。

![start-auto-change.png 85%](https://lab-static.pingcap.com/images/2026/2/10/d296c4b7f3dd1610f3c65eab275f0124798e493b.png)

> [!TIP]
> 既にブラウザで[書籍管理](${LAB:HTTPS_REVERSE_PROXY_URL}?next=${LAB:"/vscode/proxy/8000/products/manage"|urlencode})ページを開いている場合は、「自動変更を開始」ボタンをクリックする前にページを更新してください。

+ ページを開いたまま、次のステップに進みます。

### 手順 4. オンラインスキーマ変更を実行する

+ VS Code ターミナル 2で、`php artisan make:migration…` コマンドを使用して、`SHOP`.`TRANSACTIONS` テーブルに `Discount` カラムを追加するためのマイグレーションファイルを生成します。

  ```bash
  php artisan make:migration add_discount_to_transactions_table --table=transactions
  ```

  ```output
  INFO  Migration [database/migrations/2026_01_22_171252_add_discount_to_transactions_table.php] created successfully.
  ```

+ VS Codeで、新たに生成されたコードファイル `/database/migrations/` 内の `2026_01_22_171252_add_discount_to_transactions_table.php` を見つけます。ファイル名はマニュアルの例と異なる場合があります。形式は `xxxx_xx_xx_xxxxxx_add_discount_to_transactions_table.php` です。

![205_5_3_1.png 85%](https://lab-static.pingcap.com/images/2026/1/22/c95ec6163c9498ad30ac3f16926650d4130b641d.png)

+ ファイル `/database/migrations/2026_01_22_171252_add_discount_to_transactions_table.php` を更新し、`Discount` カラムの定義とロールバック操作を追加して、ファイルを保存します。ファイル名はマニュアルの例と異なる場合があります。形式は `xxxx_xx_xx_xxxxxx_add_discount_to_transactions_table.php` です。

  ```php
  <?php

  use Illuminate\Database\Migrations\Migration;
  use Illuminate\Database\Schema\Blueprint;
  use Illuminate\Support\Facades\Schema;

  return new class extends Migration
  {
    /**
      * Run the migrations.
      */
    public function up(): void
    {
        Schema::table('transactions', function (Blueprint $table) {
            //
            $table->decimal('discount', 5,2)->default(0.0);
        });
    }

    /**
      * Reverse the migrations.
      */
    public function down(): void
    {
        Schema::table('transactions', function (Blueprint $table) {
            //
            $table->dropColumn('discount');
        });
    }
  };
  ```

+ ターミナルに戻り、`php artisan migration`コマンドを使用して、`SHOP`.`TRANSACTIONS`テーブルのオンラインスキーマ変更を実行します。実行中のトランザクションはスキーマ変更をブロックしません。

  ```bash
  php artisan migrate
  ```

  ```output
  INFO  Running migrations.

  2026_01_22_171252_add_discount_to_transactions_table ................................ 845.16ms DONE
  ```

+ [書籍管理](${LAB:HTTPS_REVERSE_PROXY_URL}?next=${LAB:"/vscode/proxy/8000/products/manage"|urlencode}) ページを確認します。実行中のトランザクションはスキーマの変更によってブロックされません。

+ ターミナルで `php artisan db:table transactions` コマンドを引き続き使用して、`SHOP`.`TRANSACTIONS` テーブルの定義を表示します。`Discount` カラムが追加されていることが確認でき、プロセス全体を通してテーブル変更によるトランザクションの失敗がないことがわかります。

  ```bash
  php artisan db:table transactions
  ```

  ```output
  shop.transactions ..........................................................
  Columns ................................................................. 10
  Size ............................................................... 2.09 KB
  Engine .............................................................. InnoDB
  Collation ............................................... utf8mb4_unicode_ci

  Column ................................................................ Type
  id bigint .............................................. bigint(20) unsigned
  user_id bigint ......................................... bigint(20) unsigned
  product_id bigint ...................................... bigint(20) unsigned
  price decimal ................................................ decimal(10,2)
  buy_count int ...................................................... int(11)
  total_price decimal .......................................... decimal(18,2)
  created_at timestamp, nullable ................................... timestamp
  updated_at timestamp, nullable ................................... timestamp
  pay_type bigint ............................................... 1 bigint(20)
  discount decimal ................................................ 0 decimal(5,2)

  Index ......................................................................
  primary id .................................................. btree, primary
  user_transaction_idx user_id ......................................... btree
  product_transaction_idx product_id ................................... btree
  ```

### 手順 5. ビューの修正と新しいトランザクションリストページの確認

+ VSCodeで、Controller `APP/Http/Controllers/TransactionController.php` 内の関数 `public function index()` を修正します。この関数は「Transactions list」機能の実装に使用されます。変数 `$hasDiscount` の初期値を `false` から`true` に変更し、ファイルを保存します。

  ```php
  /**
  * Display a listing of the resource.
  */
  public function index()
  {
  //
  // Rows per page.
  $row_per_page = 10;

  // Get the transactions of this page.
  $TransactionPaginate = Transaction::OrderBy('created_at', 'desc')
  ->OrderBy('user_id','desc')
  ->with('Product')
  ->with('User')
  ->with('PayType')
  ->paginate($row_per_page);

  // Set the title and data for view.
  $binding = [
      'title' => __('shop.transaction.Transactions-list'),
      'TransactionPaginate'=> $TransactionPaginate,
  ];

  /**
  * If $hasDiscount is true, it will use the view 'Transactions.listTransactionSave.blade.php' with 'Discount' column in the its table to list the transactions.
  * If $hasDiscount is false, it will use the view 'Transactions.listTransaction.blade.php' without 'Discount' column in the its table to list the transactions.
  */
  $hasDiscount = true;
  if($hasDiscount) {
      // Sent the Transactions data to view with the column 'Discount'.
      return view('transactions.listTransactionSave', $binding);
  }
  else {
      // Sent the Transactions data to view without the column 'Discount'.
      return view('transactions.listTransaction', $binding);
  }
  ```

+ 修正後、トランザクションリストはView `resource/views/Transactions.listTransactionSave.blade.php` によってレンダリングされます。[注文履歴](${LAB:HTTPS_REVERSE_PROXY_URL}?next=${LAB:"/vscode/proxy/8000/transactions"|urlencode})で再度「注文履歴」ページにアクセスすると、`割引` カラムが存在することが確認できます。

![204_5_4_1.png 85%](https://lab-static.pingcap.com/images/2026/1/23/7f34632387547b04020ecd8d2e2a9446c60db7b5.png)

本モジュールは終了です。
