SQL Database と SQL Managed Instance での一時的な接続エラーのトラブルシューティング

適用対象:Azure SQL データベースAzure SQL Managed InstanceAzure Synapse Analytics

この記事では、クライアント アプリケーションが Azure SQL Database、Azure SQL Managed Instance、Azure Synapse Analytics とやり取りする際に発生する接続エラーと一時エラーを防止、トラブルシューティング、診断、軽減する方法について説明します。 再試行ロジックの構成方法、接続文字列の作成方法、およびその他の接続設定の調整方法について説明します。

一時エラー (一過性の障害)

一時エラーは、一過性の障害とも呼ばれ、基になる原因はすぐ自動的に解決されます。 一時エラーを起こす偶発的原因として、Azure システムが、各種ワークロードの負荷分散を行うために行うハードウェア リソースの瞬間的切り替えがあります。 この再構成イベントのほとんどは 60 秒以内に完了します。 この再構成の進行中、SQL Database のデータベースへの接続で問題が発生する場合があります。 データベースに接続するアプリケーションは、これらの一時エラーを想定して構築する必要があります。 これに対処するには、アプリケーション エラーとしてユーザーに表示するのではなく、コードに再試行ロジックを実装します。

クライアント プログラムで ADO.NET を使用している場合、SqlException のスローによって一時エラーが報告されます。

接続とコマンド

以下の条件に応じて、SQL Database と SQL Managed Instance の接続を再試行するか、再度確立します。

  • 接続試行中に一時エラーが発生する

数秒間の時間を置いて、接続を再試行します。

  • SQL Database と SQL Managed Instance のクエリ コマンドの実行中に一時エラーが発生する

コマンドをすぐに再試行しないでください。 少し時間を置いて、新たに接続を確立します。 その後、コマンドを再試行します。

一時エラーの再試行ロジック

一時エラーが多少なりとも生じるクライアント プログラムは、再試行ロジックを組み込むことで堅牢性を高めることができます。 ご使用のプログラムがサード パーティのミドルウェアを介して SQL Database のデータベースと通信する場合、一時エラーに対応した再試行ロジックがそのミドルウェアに備わっているかどうかをベンダーに確認してください。

再試行の原則

  • 一時的なエラーである場合は、再試行して接続を開いてください。
  • 一時エラーで失敗した SQL Database または SQL Managed Instance の SELECT ステートメントは、そのまま再試行しないでください。 新しい接続を確立してから、SELECT を再試行してください。
  • SQL Database または SQL Managed Instance の UPDATE ステートメントが一時エラーで失敗した場合、新たに接続を確立したうえで UPDATE を再試行してください。 データベース トランザクション全体が完了したのか、トランザクション全体がロールバックされたのかを再試行ロジックで確認する必要があります。

再試行に関するその他の注意点

  • 業務終了時刻に自動的に起動され翌朝までに完了するバッチ プログラムは、再試行間隔をある程度長めにしても問題ありません。
  • 人間の心理として、待ち時間があまり長いと途中でキャンセルされる可能性があります。ユーザー インターフェイス プログラムはその点を考慮するようにしてください。 数秒おきに再試行するようなやり方は避けてください。そのような方法を用いると、大量の要求でシステムが処理しきれなくなる可能性があります。

再試行の間隔を長くする

最初に再試行する前に、5 秒間待つことをお勧めします。 5 秒未満で再試行すると、クラウド サービスに過度の負荷がかかるおそれがあります。 再試行するたびに、待ち時間を大幅に長くし、最大 60 秒待つ必要があります。

ADO.NET を使用するクライアントのブロック期間については、接続プール (ADO.NET) に関するページを参照してください。

加えて、最大再試行回数を設定し、プログラムが自動的に終了するように配慮する必要があります。

再試行ロジックを含んだコード サンプル

再試行ロジックを含むコード サンプルについては次の記事をご覧ください。

再試行ロジックのテスト

再試行ロジックをテストするには、プログラムの実行中に修復可能なエラーをシミュレートする (人為的に発生させる) 必要があります。

ネットワークから切断することによるテスト

再試行ロジックをテストする手段として、プログラムの実行中にクライアント コンピューターをネットワークから切断する方法が挙げられます。 エラーは次のとおりです。

  • SqlException.Number = 11001
  • メッセージ:"そのようなホストは不明です"

最初の再試行の一環として、クライアント コンピューターをネットワークに再接続してから接続を試行することができます。

このテストを実践するには、コンピューターをネットワークから切断した後でプログラムを起動します。 その後プログラムは、ランタイム パラメーターを通じて次の処理を行います。

  • エラーのリストに対して一時的に 11001 を追加し、一過性と見なします。
  • 通常と同様に初回接続を試みます。
  • エラーが捕捉された後、11001 をリストから削除します。
  • コンピューターをネットワークに接続するようにユーザーに通知するメッセージが表示されます。
  • Console.ReadLine メソッドか、[OK] ボタンを含むダイアログのいずれかを使用して、以降の実行を一時停止します。 コンピューターがネットワークに接続された後に、ユーザーが Enter キーを押します。
  • 再度接続を試みます。

接続時に間違った綴りのユーザー名を使用することによるテスト

意図的に間違ったユーザー名を使って初回接続を試みます。 エラーは次のとおりです。

  • SqlException.Number = 18456
  • メッセージ:"ユーザー WRONG_MyUserName はログインできませんでした"

最初の再試行のときに、プログラムでスペルミスを修正してから接続を試みてください。

このテストを実践するには、プログラムでランタイムパラメーターを通じて次の処理を行います。

  • エラーのリストに対して一時的に 18456 を追加し、一過性と見なします。
  • 意図的に 'WRONG_' をユーザー名に追加します。
  • エラーが捕捉された後、18456 をリストから削除します。
  • ユーザー名から 'WRONG_' を削除します。
  • 再度接続を試みます。

接続再試行用の .NET SqlConnection パラメーター

.NET Framework クラスの System.Data.SqlClient.SqlConnection を使用してクライアント プログラムから Azure SQL Database のデータベースに接続する場合は、接続再試行機能を活用できるように .NET 4.6.1 以降のバージョン (または .NET Core) を使用してください。 この機能に関する詳細は、「SqlConnection.ConnectionString Property」を参照してください。

接続文字列SqlConnection オブジェクト用に作成するときは、次のパラメーター間で値を調整します。

  • ConnectRetryCount: 既定値は 1 です。 範囲は 0 から 255 です。
  • ConnectRetryInterval: 既定は 10 秒です。 範囲は 1 から 60 です。
  • Connection Timeout: 既定値は 15 秒です。 範囲は 0 から 2147483647 です。
  • Command Timeout: 既定は 30 秒です。 範囲は 0 から 2147483647 です。

接続の回復性には、接続の再試行設定 (ConnectRetryCountConnectRetryInterval) が適用されます。 接続の回復性には、次の種類が含まれます。

  • オープン接続の回復性は、最初の SqlConnection.Open または OpenAsync() メソッドを参照します。 最初の接続試行は、try 0 としてカウントされます。 ConnectRetryCount は、後続の再試行に適用されます。 したがって、接続 0 が失敗した場合 (すぐには発生しない可能性があります)、ConnectRetryInterval が最初に適用され、その後に ConnectRetryCount (および ConnectRetryInterval) が試行されます。 すべての再試行を利用するには、Connection Timeout プロパティですべての試行に時間を指定する必要があります。

  • アイドル接続の回復性とは、切断された既存のアイドル状態の接続の自動検出と再接続を指します。 切断されたアイドル状態の接続を再接続する最初の試行は、最初の再試行としてカウントされます。 すべての再試行を利用するには、Command Timeout プロパティですべての試行に時間を指定する必要があります。

例: ConnectRetryCount パラメーターと ConnectRetryInterval パラメーターの値を次に示します。

ConnectRetryCount: 3 ConnectRetryInterval: 10 秒

次のシナリオで、これらの値がどのように使用されるかをご覧ください。

シナリオ: 新しい接続

4:10:00 - Connection.Open() - ゼロ試行

4:10:01 - 接続エラーが検出されました

4:10:11 - 再試行 1 -->ConnectRetryInterval の後に最初の再試行が行われます

4:10:21 - 再試行 2

4:10:31 - 再試行 3

このシナリオでは、選択した値が次の条件を満たす必要があります。
Connection Timeout > = ConnectRetryCount * ConnectionRetryInterval

たとえば、回数が 3、間隔が 10 秒、タイムアウトが 29 秒のみの場合、次のようになるため、システムの 3 回目の最後の接続の試行には十分な時間が与えられません。

29 < 3 * 10

シナリオ: アイドル状態の接続

ConnectRetryCount: 3 ConnectRetryInterval: 10 秒

4:10:00 - コマンドの実行時に接続の切断が検出されました

4:10:00 - 再試行 1 -->最初の再試行はすぐに行われます

4:10:10 - 再試行 2

4:10:20 - 再試行 3

これは最初の接続ではありません。 そのため、Connection Timeout は適用されません。 ただし、接続の回復はコマンドの実行中に発生するため、Command Timeout 設定が適用されます。 Command Timeout: 既定は 30 秒です。 ただし、接続の復旧は一般的な状況では高速ですが、間欠的に停止すると、復旧にコマンドの実行時間がかかることがあります。

このシナリオでは、アイドル状態の接続回復の再試行を最大限に活用する場合は、選択した値が次の条件を満たす必要があります。
Command Timeout > (ConnectRetryCount - 1) * ConnectionRetryInterval

たとえば、カウントが 3 で間隔が 10 秒の場合、20 秒未満のコマンド タイムアウト値では、3 回目と最後の再試行が接続するのに十分な時間が与えられません: (3 - 1) * 10 = 20`

また、接続の復旧後にコマンド自体の実行に時間がかかることを考慮してください。

注意

これらのシナリオで提供される期間の値は、デモンストレーション専用です。 両方のシナリオでの実際の検出時間は、基になるインフラストラクチャによって異なります。

接続とコマンド

ConnectRetryCount パラメーターと ConnectRetryInterval パラメーターを使用すると、プログラムに制御を返すなど、プログラムへの通知や介入なしに、SqlConnection オブジェクトで接続操作を再試行できます。 再試行は次の状況で発生することがあります。

  • SqlConnection.Open メソッド呼び出し
  • SqlConnection.Execute メソッド呼び出し

これには注意が必要です。 "クエリ" の実行中に一時エラーが発生した場合、SqlConnection オブジェクトで接続操作が再試行されません。 間違いなくクエリは再試行されません。 ただし、実行するクエリを送信する前に、 SqlConnection ですばやく接続が確認されます。 簡単なチェックで接続の問題が検出された場合、 SqlConnection で接続操作が再試行されます。 再試行に成功すると、実行するクエリが送信されます。

ConnectRetryCount をアプリケーションの再試行ロジックと組み合わせて使用する必要があるかどうか

アプリケーションにカスタムの堅牢な再試行ロジックが組み込まれていると仮定します。 このロジックでは、接続操作が 4 回再試行されます。 ConnectRetryIntervalConnectRetryCount = 3 を接続文字列に追加すると、再試行回数が 4 * 3 = 12 に増加します。 このように何回も再試行するのは、適切ではない可能性があります。

SQL Database のデータベースへの接続

接続:接続文字列

データベースに接続するために必要な接続文字列は、SQL Server への接続に使用される文字列とは若干異なります。 データベースの接続文字列は Azure ポータルからコピーすることができます。

Azure ポータルから接続文字列を取得する

Azure Portal を使って、クライアント プログラムが Azure SQL Database と対話するために必要な接続文字列を取得します。

  1. [すべてのサービス]>[SQL データベース] の順にクリックします。

  2. [SQL Database]ウィンドウの左上近辺にあるフィルター テキスト ボックスにデータベースの名前を入力します。

  3. お使いのデータベースの行を選びます。

  4. データベースのウィンドウが表示されたら、画面を見やすくするために[最小化]ボタンを選択し、閲覧とデータベースフィルター処理に使用したウィンドウを折りたたみます。

  5. データベースのウィンドウで、[データベース接続文字列の表示]を選択します。

  6. 適切な接続文字列をコピーします。 つまり、ADO.NET 接続ライブラリを使用する場合は、[ADO.NET] タブから適切な文字列をコピーします。

    Copy the ADO connection string for your database

  7. 必要に応じて、接続文字列を編集します。 つまり、接続文字列にパスワードを挿入したり、ユーザー名またはサーバー名が長すぎる場合はユーザー名から "@<servername>" を削除します。

  8. 特定の形式の接続文字列情報を、クライアント プログラム コードに貼り付けます。

詳しくは、「接続文字列と構成ファイル」をご覧ください。

接続:IP アドレス

SQL Database は、クライアント プログラムのホストとなるコンピューターの IP アドレスからの通信を許可するように構成する必要があります。 この構成を設定するには、Azure Portal を通じてファイアウォール設定を編集します。

IP アドレスの構成を怠った場合、必要な IP アドレスを示した役立つエラー メッセージが表示され、プログラムが失敗します。

  1. Azure portal にサインインします。

  2. 左側の一覧で、[すべてのサービス] を選びます。

  3. スクロールして、[SQL Server] を選びます。

    Find your Azure SQL Database server in the portal

  4. フィルター テキスト ボックスで、サーバーの名前の入力を開始します。 行が表示されます。

  5. お使いのサーバーの行を選びます。 サーバーのウィンドウが表示されます。

  6. サーバーのウィンドウで、[設定]を選択します。

  7. [ファイアウォール] を選びます。

    Select Settings > Firewall

  8. [クライアント IP の追加] を選びます。 最初のテキスト ボックスに、新しい規則の名前を入力します。

  9. 有効にする範囲の IP アドレスの値を入力します。

    • 低い方の値の最後を .0、高い方の値の最後を .255 にしておくと便利です。
  10. [保存] を選択します。

詳細については、SQL Database のファイアウォール設定の構成に関するページを参照してください。

接続:Port

通常、必要な設定は、クライアント プログラムのホストとなるコンピューターのポート 1433 の送信方向を開放するだけです。

たとえば、クライアント プログラムが Windows コンピューターでホストされている場合、そのホストの Windows ファイアウォールを使用してポート 1433 を開放することができます。

  1. [コントロール パネル] を開きます。
  2. [すべてのコントロール パネル項目]>[Windows ファイアウォール]>[詳細設定]>[送信の規則]>[アクション]>[新しい規則] の順に選択します。

クライアント プログラムが Azure の仮想マシン (VM) でホストされている場合は、ADO.NET 4.5 用の 1433 以外のポートと SQL Database に関するページを参照してください。

データベースのポートと IP アドレスの構成に関する背景情報については、Azure SQL Database のファイアウォールに関するページを参照してください。

接続:ADO.NET 4.6.2 以降

プログラムで System.Data.SqlClient.SqlConnection などの ADO.NET クラスを使用して SQL Database に接続する場合は、.NET Framework バージョン 4.6.2 以降を使用することをお勧めします。

ADO.NET 4.6.2 で開始する場合

  • Azure SQL に対して接続を開く試みが直ちに再試行されます。そのため、クラウド対応アプリのパフォーマンスが向上します。

ADO.NET 4.6.1 で開始する場合

  • SQL Database では、SqlConnection.Open メソッドを使用して接続を開くと信頼性が向上します。 Open メソッドには、接続タイムアウト期間内の特定のエラーを対象に、一過性の障害に対応するベスト エフォート再試行メカニズムが組み込まれました。
  • 接続プールがサポートされているため、プログラムに割り当てた接続オブジェクトが正常に動作しているかどうかを効率的に検証することが可能です。

接続プールから取得した接続オブジェクトを使用するとき、すぐに使用しないのであれば、プログラムで一時的に接続を閉じることをお勧めします。 接続を再度開く処理負荷はわずかですが、新しい接続を作成する負荷は大きくなります。

ADO.NET 4.0 以前のバージョンを使用する場合、最新の ADO.NET. にアップグレードすることをお勧めします。 2018 年 8 月の時点で、ADO.NET 4.6.2 のダウンロードが可能になりました。

診断

診断:ユーティリティから接続できるかどうかをテストする

プログラムから SQL Database のデータベースに接続できないときの診断方法として 1 つ考えられるのは、ユーティリティ プログラムを使用して接続する方法です。 プログラムで使用しているのと同じライブラリを使用して接続するユーティリティがあれば理想的です。

任意の Windows コンピューターで、次のユーティリティを試すことができます。

  • SQL Server Management Studio (ssms.exe)。ADO.NET を使用して接続します。
  • sqlcmd.exeODBC を使用して接続します。

プログラムが接続された後に、短い SQL SELECT クエリが正しく動作するかどうかをテストしてください。

診断:開放ポートを確認する

ポートの問題が原因で接続に失敗している可能性がある場合は、ポートの構成に関するレポート作成に対応したユーティリティをご使用のコンピューターで実行してください。

Linux では、次のユーティリティが役に立つ場合があります。

  • netstat -nap
  • nmap -sS -O 127.0.0.1:例の値を実際の IP アドレスに変更してください。

Windows では PortQry.exe ユーティリティが利用できます。 以下は、SQL Database のデータベースのポートの状況の照会をノート PC 上で実行する例を示しています。

[C:\Users\johndoe\]
>> portqry.exe -n johndoesvr9.database.windows.net -p tcp -e 1433

Querying target system called: johndoesvr9.database.windows.net

Attempting to resolve name to IP address...
Name resolved to 23.100.117.95

querying...
TCP port 1433 (ms-sql-s service): LISTENING

[C:\Users\johndoe\]
>>

診断:エラーのログを記録する

断続的な問題は、過去数日から数週間にわたる一般的なパターンを検出することによって診断できる場合が多々あります。

診断には、クライアントで発生したエラーのログが役立ちます。 SQL Database が内部的に記録するエラー データとそれらのログ エントリを相互に関連付けることも可能です。

Enterprise Library 6 (EntLib60) には、ログ記録をサポートする .NET マネージド クラスがあります。 詳細については、「5 - As easy as falling off a log: Use the Logging Application Block (5 - きわめて簡単: Logging アプリケーション ブロックの使用)」を参照してください。

診断:エラーの発生をシステム ログで調べる

以下に示したのは、エラー ログや各種情報を照会する Transact-SQL SELECT ステートメントの例です。

ログのクエリ 説明
SELECT e.*
FROM sys.event_log AS e
WHERE e.database_name = 'myDbName'
AND e.event_category = 'connectivity'
AND 2 >= DateDiff
  (hour, e.end_time, GetUtcDate())
ORDER BY e.event_category,
  e.event_type, e.end_time;
sys.event_log ビューには、一時エラーや接続障害を引き起こす可能性のあるものを含む、個々のイベントに関する情報が表示されます。

理想的には、start_timeend_time の値を、クライアント プログラムに問題が発生した時間の情報に関連付けます。

このクエリを実行するには、"マスター" データベースに接続する必要があります。
SELECT c.*
FROM sys.database_connection_stats AS c
WHERE c.database_name = 'myDbName'
AND 24 >= DateDiff
  (hour, c.end_time, GetUtcDate())
ORDER BY c.end_time;
sys.database_connection_stats ビューには、イベントの種類ごとに集計されたカウントが表示され、詳しい診断を行うことができます。

このクエリを実行するには、"マスター" データベースに接続する必要があります。

診断:SQL Database のログから問題のイベントを検索する

SQL Database のログで問題のイベントに関するエントリを検索することができます。 master データベースで次の Transact-SQL SELECT ステートメントを試してみてください。

SELECT
   object_name
  ,CAST(f.event_data as XML).value
      ('(/event/@timestamp)[1]', 'datetime2')                      AS [timestamp]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="error"]/value)[1]', 'int')             AS [error]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="state"]/value)[1]', 'int')             AS [state]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="is_success"]/value)[1]', 'bit')        AS [is_success]
  ,CAST(f.event_data as XML).value
      ('(/event/data[@name="database_name"]/value)[1]', 'sysname') AS [database_name]
FROM
  sys.fn_xe_telemetry_blob_target_read_file('el', null, null, null) AS f
WHERE
  object_name != 'login_event'  -- Login events are numerous.
  and
  '2015-06-21' < CAST(f.event_data as XML).value
        ('(/event/@timestamp)[1]', 'datetime2')
ORDER BY
  [timestamp] DESC
;

sys.fn_xe_telemetry_blob_target_read_file から返される行の例

次の例は、どのような行が返されるかを示しています。 ここに示した行では null 値が表示されていますが、null 以外の場合も多くあります。

object_name                   timestamp                    error  state  is_success  database_name

database_xml_deadlock_report  2015-10-16 20:28:01.0090000  NULL   NULL   NULL        AdventureWorks

Enterprise Library 6

Enterprise Library 6 (EntLib60) は、.NET クラスのフレームワークです。クラウド サービス (SQL Database もその 1 つ) に対する堅牢なクライアントをこのフレームワークを使って実装することができます。 EntLib60 の利便性が発揮される個々の領域の説明については、「Enterprise Library 6 - April 2013 (Enterprise Library 6 – 2013 年 4 月)」を参照してください。

一時エラーを処理するための再試行ロジックは、EntLib60 を利用できる 1 つの領域です。 詳細については、「4 - Perseverance, Secret of All Triumphs:Use the Transient Fault Handling Application Block」をご覧ください。

Note

EntLib60 のソース コードは、ダウンロード センターから入手できます。 EntLib に対して機能の更新や保守目的での更新を行う予定はありません。

一時エラーと再試行に関連した EntLib60 のクラス

再試行ロジックで特に利用する機会の多い EntLib60 のクラスは次のとおりです。 これらのクラスはすべて、Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling 名前空間に属しています。

名前空間 Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling:

  • RetryPolicy クラス
    • ExecuteAction メソッド
  • ExponentialBackoff クラス
  • SqlDatabaseTransientErrorDetectionStrategy クラス
  • ReliableSqlConnection クラス
    • ExecuteCommand メソッド

Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.TestSupport名前空間:

  • AlwaysTransientErrorDetectionStrategy クラス
  • NeverTransientErrorDetectionStrategy クラス

EntLib60 に関する情報は以下のリンクから入手できます。

EntLib60:Logging ブロック

  • ログブロックは、非常柔軟で構成可能なソリューションであり、次の用途で使用できます。
    • ログ メッセージをさまざまな場所に作成して保存する。
    • メッセージを分類したりフィルタリングしたりする。
    • デバッグやトレース、監査要件、全般的なログ要件に利用できるコンテキスト情報を収集する。
  • Logging ブロックは、ログ出力先が備えるログ機能を抽象化したものです。対象となるログ ストアの場所や種類に関係なく、アプリケーション コードの一貫性を確保することができます。

詳細については、「5 - As easy as falling off a log: Use the Logging Application Block (5 - きわめて簡単: Logging アプリケーション ブロックの使用)」を参照してください。

EntLib60 IsTransient メソッドのソース コード

以下に示したのは、SqlDatabaseTransientErrorDetectionStrategy クラスの IsTransient メソッドの C# ソース コードです。 どのようなエラーが一過性で、再試行する価値があるかは、このソース コードを見るとはっきりわかります (2013 年 4 月時点)。

public bool IsTransient(Exception ex)
{
  if (ex != null)
  {
    SqlException sqlException;
    if ((sqlException = ex as SqlException) != null)
    {
      // Enumerate through all errors found in the exception.
      foreach (SqlError err in sqlException.Errors)
      {
        switch (err.Number)
        {
            // SQL Error Code: 40501
            // The service is currently busy. Retry the request after 10 seconds.
            // Code: (reason code to be decoded).
          case ThrottlingCondition.ThrottlingErrorNumber:
            // Decode the reason code from the error message to
            // determine the grounds for throttling.
            var condition = ThrottlingCondition.FromError(err);

            // Attach the decoded values as additional attributes to
            // the original SQL exception.
            sqlException.Data[condition.ThrottlingMode.GetType().Name] =
              condition.ThrottlingMode.ToString();
            sqlException.Data[condition.GetType().Name] = condition;

            return true;

          case 10928:
          case 10929:
          case 10053:
          case 10054:
          case 10060:
          case 40197:
          case 40540:
          case 40613:
          case 40143:
          case 233:
          case 64:
            // DBNETLIB Error Code: 20
            // The instance of SQL Server you attempted to connect to
            // does not support encryption.
          case (int)ProcessNetLibErrorCode.EncryptionNotSupported:
            return true;
        }
      }
    }
    else if (ex is TimeoutException)
    {
      return true;
    }
    else
    {
      EntityException entityException;
      if ((entityException = ex as EntityException) != null)
      {
        return this.IsTransient(entityException.InnerException);
      }
    }
  }

  return false;
}

次のステップ