Drupalで大量のデータを処理する技

# 概要
なぜ、大量のデータを処理する際に注意しないといけないかというとタイムアウトする可能性があるからです。
もちろん、php.iniの設定を調整する、または、書いた処理の効率を見直すだけで解決する事が多いですが、それでもデータは溜まり続けるので、より進んだ方法で対処する方法の紹介です。


## 本題
Drupalでは、「Batch API」なるものが用意されています。
当初、Batchって聞くと「cron」とかで、深夜とかに定期的に処理を走らせるとか、そういうことかなと思いました。

https://www.drupal.org/docs/drupal-apis/batch-api/batch-api-overview

このAPIはそうではなくて、普通のフォームや、コントローラー、フックなどで扱え、例えば、フォームを更新したときに、〇〇件処理して終わったらまた、再度〇〇件処理する。
要は、分割した件数が終わるまでそれを繰り返して、タイムアウトしづらいようにするというものだと認識しています。(もし、間違っていればご指摘ください)

特別なものではなく普通のフォームに付け加える形で使えるので、後からも導入しやすい所がいいなと思いました。

### 解説
↓FormBaseを継承した普通のフォームにバッチapiをつけます。
```php:BlockUsersBatchForm
/**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $this->startBatchProcess($form_state->getValue('username'), $this->currentUser()->id());
    $this->messenger()->addStatus('ユーザーをブロックするバッチ処理が開始されました。');
    $form_state->setRedirect('<current>');
  }

  /**
   * ユーザーをブロックするバッチ プロセスを開始する.
   */
  private function startBatchProcess(string $username, int $current_user_id): void {
    $query = $this->entityTypeManager->getStorage('user')
      ->getQuery()->condition('name', $username, 'CONTAINS')
      ->condition('uid', $current_user_id, '!=')
      ->condition('status', 1)->accessCheck(FALSE);
    $user_ids = $query->execute();

    $chunks = array_chunk($user_ids, 10);
    $operations = [];

    foreach ($chunks as $chunk) {
      $operations[] = [
        '\Drupal\batch_users\Form\BlockUsersBatchForm::processBatch',
        [$chunk],
      ];
    }

    $batch = [
      'title' => 'ブロックユーザー',
      'operations' => $operations,
      'finished' => '\Drupal\batch_users\Form\BlockUsersBatchForm::finishBatch',
    ];

    batch_set($batch);
  }
```

この、「Batch API」を利用して特定のユーザー名が含まれているユーザーをブロックするモジュールを開発しました。

ユーザーIDを取得してarray_chunkで分割させた上でループして配列に格納した上で、「batch_set」関数を
呼び出します。これでバッチが使えるようになります。
batch_setに渡す配列に呼び出したいメソッド名を記述します。

* スタティック「processBatch」メソッド → ループで処理するメインの処理 
* スタティック「finishBatch」メソッド → 完了時の処理

```php:BlockUsersBatchForm
/**
   * ユーザーの各バッチを処理します.
   */
  public static function processBatch(array $user_ids, array &$context): void {
    foreach ($user_ids as $uid) {
      $user = User::load($uid);
      if ($user) {
        // ステータスをブロックする.
        $user->block();
        $user->save();
      }
    }
    $context['message'] = t('@count ユーザーに対するユーザーのブロックを処理しています。', ['@count' => count($user_ids)]);
  }

  /**
   * バッチ完了を処理します.
   */
  public static function finishBatch(bool $success, array $results, array $operations): RedirectResponse {
    // staticのため、手続き的に記述.
    $messenger = \Drupal::messenger();
    $logger = \Drupal::logger('batch_users');
    if ($success) {
      $messenger->addMessage('ユーザーをブロックするための処理が正常に完了しました。');
    }
    else {
      $error_operation = reset($operations);
      $message = t('処理中にエラーが発生しました: %error_operation 引数: @arguments', [
        '%error_operation' => $error_operation[0],
        '@arguments' => print_r($error_operation[1], TRUE),
      ]);
      $messenger->addError($message);
      $logger->error($message);
    }
    $url = Url::fromRoute('batch_users.block_users_batch')->toString();
    return new RedirectResponse($url);
  }
```

batch_setが正しく機能していると、処理が走る時にプログレスバーが表示されます。
(一瞬なのでスクショとれませんが)
モジュールインストールするときのあれと同じものです。


## 余談

システム系なら、開発時は、あまりデータが入っていなかったりして効率が悪い処理を書いていて、タイムアウトして処理を見直す
または、予測していないぐらいの大量のデータが記録されて、php.iniの設定値を調整するとかはよくあるのかなと思います。


cms系なら、あまりタイムアウトしたりすることはない気がしますし、意識することはないと思います。
ただ、ワードプレスだとリビジョンが溜まりすぎたり、なんでも、「wp_posts」テーブルに入る関係上、知らないうちにプラグイン設定値のデータが大量に「wp_posts」テーブルに入っていて、
何か作業をした際にタイムアウトするみたいなことが、何年も使い続けているような、サイトでごく稀にあるように思います。(ワードプレス特有の問題だと思いますが)

こういうAPIをcmsが用意してくれているっていうのが、珍しい気がしますし、Drupalが大規模なものも対応できると言われたりする所なのかなと思ったりします。