データベースに何かが存在するかどうかを確認し、高速で失敗するか、データベース例外を待つ必要がありますか


32

2つのクラスがある:

public class Parent 
{
    public int Id { get; set; }
    public int ChildId { get; set; }
}

public class Child { ... }

割り当てるときChildIdParent、それが例外をスローするDB用DBまたは待機中に存在する場合、私が最初に確認する必要がありますか?

例(Entity Framework Coreを使用):

注:これらの種類のチェックは、Microsoftの公式ドキュメントでもインターネット全体で行われます:https : //docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using- mvc / handling-concurrency-with-the-entity-framework-in-an-asp-mvc-application#modify-the-department-controllerしかし、追加の例外処理がありますSaveChanges

また、このチェックの主な目的は、APIのユーザーにわかりやすいメッセージと既知のHTTPステータスを返すことであり、データベースの例外を完全に無視することではないことに注意してください。そして、例外がスローされる唯一の場所は、内部にあるSaveChangesSaveChangesAsyncを呼び出すときに例外がありません...コールFindAsyncまたはAny。そのため、子が存在するが以前に削除されていた場合SaveChangesAsync、同時実行例外がスローされます。

これは、foreign key violation「ID {parent.ChildId}の子が見つかりませんでした」と表示するために、例外の書式設定がはるかに困難になるという事実のために行いました。

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    // is this code redundant?
   // NOTE: its probably better to use Any isntead of FindAsync because FindAsync selects *, and Any selects 1
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null)
       return NotFound($"Child with id {parent.ChildId} could not be found.");

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        

    return parent;
}

対:

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    _db.Parents.Add(parent);
    await _db.SaveChangesAsync();  // handle exception somewhere globally when child with the specified id doesn't exist...  

    return parent;
}

Postgresの2番目の例は23503 foreign_key_violationエラーをスローします:https : //www.postgresql.org/docs/9.4/static/errcodes-appendix.html

EFのようなORMでこのように例外を処理する欠点は、特定のデータベースバックエンドでのみ機能することです。エラーコードが変更されるため、SQLサーバーまたは他の何かに切り替えたい場合、これはもう機能しません。

例外をエンドユーザー向けに適切にフォーマットしないと、開発者以外には見られたくないものが明らかになる可能性があります。

関連:

https://stackoverflow.com/questions/6171588/preventing-race-condition-of-if-exists-update-else-insert-in-entity-framework

https://stackoverflow.com/questions/4189954/implementing-if-not-exists-insert-using-entity-framework-without-race-conditions

https://stackoverflow.com/questions/308905/should-there-be-a-transaction-for-read-queries


2
あなたの研究を共有することは皆を助けます。試したことと、それがニーズに合わなかった理由を教えてください。これは、あなたが時間をかけて自分自身を助けようとしていること、明白な答えを繰り返すことから私たちを救うこと、そして何よりもあなたがより具体的で関連性のある答えを得ることを助けることを示しています。参照してください掲載する方法
GNAT

5
他の人が述べたように、NotFoundのチェックと同時にレコードが挿入または削除される可能性があります。そのため、最初にチェックすることは受け入れられない解決策のようです。他のデータベースバックエンドに移植できないPostgres固有の例外処理の作成が心配な場合は、データベース固有のクラス(SQL、Postgresなど)によってコア機能を拡張できるように例外ハンドラーを構築してください
billrichards

3
コメントを見て、私はこれを言う必要があります:決まり文句でストップ思考を。「フェールファースト」は、盲目的に従うことができる、または従うべき孤立したコンテキスト外のルールではありません。それは経験則です。あなたが実際に達成しようとしていることを常に分析し、それがその目標を達成するのに役立つかどうかを考慮して、あらゆるテクニックを検討してください。「フェイルファースト」は、意図しない副作用を防ぐのに役立ちます。さらに、「高速で失敗する」とは、実際には「問題が検出されるとすぐに失敗する」という意味です。問題が検出されるとすぐに両方の手法が失敗するため、他の考慮事項を検討する必要があります。
jpmc26

1
@Konrad例外はそれと何の関係があるのですか?競合状態をコード内に存在するものと考えるのはやめてください。それは宇宙の財産です。完全に制御しないリソース(例:ダイレクトメモリアクセス、共有メモリ、データベース、REST API、ファイルシステムなど)に何度も触れ、変更されないと予想されるものは、潜在的な競合状態を持っています。ヘック、私たち例外さえも持たないCでこれを扱います。少なくとも1つのブランチがそのリソースの状態に干渉している場合、制御していないリソースの状態で分岐しないでください。
ジャレッドスミス

1
@DanielPryden私の質問では、データベース例外を処理したくないとは言いませんでした(例外は避けられないことを知っています)。多くの人が誤解していると思うので、Web API(エンドユーザーが読むために)のようなフレンドリーなエラーメッセージが欲しいと思いましたChild with id {parent.ChildId} could not be found.。そして、この場合、「外部キー違反」のフォーマットが悪いと思います。
コンラッド

回答:


3

むしろ混乱した質問ですが、はい、DB例外を処理するだけでなく、最初に確認する必要があります。

まず、あなたの例では、データ層にいて、データベース上で直接EFを使用してSQLを実行しています。コードは実行と同等です

select * from children where id = x
//if no results, perform logic
insert into parents (blah)

あなたが提案している代替案は次のとおりです。

insert into parents (blah)
//if exception, perform logic

例外を使用して条件付きロジックを実行することは遅く、普遍的に眉をひそめます。

競合状態があるため、トランザクションを使用する必要があります。しかし、これはコードで完全に行うことができます。

using (var transaction = new TransactionScope())
{
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null) 
    {
       return NotFound($"Child with id {parent.ChildId} could not be found.");
    }

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        
    transaction.Complete();

    return parent;
}

重要なことは、自問することです。

「この状況が発生すると思いますか?」

そうでない場合は、必ず挿入して例外をスローします。ただし、発生する可能性のある他のエラーと同様に、例外を処理するだけです。

発生することが予想される場合、例外ではなく、最初に子が存在するかどうかを確認し、存在しない場合は適切なフレンドリーメッセージで応答する必要があります。

編集 -これについては多くの論争があるようです。ダウン投票する前に考慮してください:

A. FK制約が2つある場合はどうなりますか。どのオブジェクトが欠落していたかを判断するために例外メッセージを解析することを推奨しますか?

B.ミスがある場合、実行されるSQLステートメントは1つだけです。2番目のクエリの追加費用が発生するのはヒットのみです。

C.通常、Idは代理キーになります。1つを知っていて、それがDB上にあるかどうかよくわからない状況を想像するのは困難です。チェックは奇妙だろう。しかし、ユーザーが自然なキーを入力した場合はどうでしょうか?それは存在しない可能性が高い


コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
maple_shaft

1
これはまったく間違っており、誤解を招く恐れがあります。私が常に戦わなければならない悪いプロを生み出すのは、このような答えです。SELECTはテーブルをロックしないため、SELECTとINSERT、UPDATE、またはDELTEの間でレコードが変更される場合があります。だから、それは粗末なソフトウェア設計が貧弱であり、本番環境で起こるのを待っている事故です。
ダニエルロボ

1
@DanielLoboトランザクションスコープはそれを修正します
ユアン

1
あなたが私を信じていないなら、それをテストしてください
ユアン

1
@yushaここにコードがあります
ユアン

111

一意性を確認してから設定することはアンチパターンです。IDがチェック時間と書き込み時間の間に同時に挿入されることは常に起こり得ます。データベースには、制約やトランザクションなどのメカニズムを介してこの問題に対処する機能が備わっています。ほとんどのプログラミング言語はそうではありません。したがって、データの一貫性を重視する場合は、エキスパート(データベース)に任せてください。つまり、挿入を実行し、例外が発生した場合は例外をキャッチします。


34
確認と失敗は、「試行」よりも速くなく、最善を願っています。前者は、システムによって実装および実行される2つの操作と、DBによって2つ実行されることを意味しますが、最新の操作はそのうちの1つのみを意味します。チェックはDBサーバーに委任されます。また、ネットワークへのホップが1つ少なくなり、DBが参加するタスクが1つ少なくなります。DBへのもう1つのクエリは手頃だと思うかもしれませんが、よく考えることを忘れることがよくあります。クエリを100回以上トリガーする同時実行性が高いと考えてください。DBへのトラフィック全体が重複する可能性があります。それが重要かどうかはあなた次第です。
ライヴ

6
@Konrad私の立場では、デフォルトで正しい選択はそれ自体で失敗する1つのクエリであり、それ自体を正当化する証拠の負担がある別のクエリプリフライトアプローチです。「問題になる」ことに関しては 、トランザクションを使用する、ToCToUエラーに対して安全であることを確認します。投稿されたコードから私には明らかではありませんが、そうでない場合は、カチカチ音を立てる爆弾が実際に爆発するずっと前に問題になるという方法がすでに問題になっています。
mtraceur

4
@Konrad EF Coreは、チェックと挿入の両方を暗黙的に1つのトランザクションに入れるわけではないため、明示的に要求する必要があります。トランザクションがなければ、データベースの状態はチェックと挿入の間で変化する可能性があるため、最初のチェックは無意味です。トランザクションを使用しても、データベースが自分の足元で変更されるのを防ぐことはできません。数年前、OracleでEFを使用する問題に遭遇しました。DBではサポートされていますが、エンティティはトランザクション内の読み取りレコードのロックをトリガーせず、挿入のみがトランザクションとして処理されました。
ミンダミンダー

3
「一意性を確認してから設定することはアンチパターンです」とは言いません。これは、他に変更が行われていないと想定できるかどうか、および存在しない場合にチェックがより有用な結果(実際には読者にとって何かを意味するエラーメッセージでさえ)を生成するかどうかに大きく依存します。同時Web要求を処理するデータベースでは、いいえ、他の変更が発生しないことを保証することはできませんが、合理的な仮定である場合があります。
jpmc26

5
最初に一意性をチェックしても、起こりうる障害を処理する必要がなくなるわけではありません。一方、アクションが複数の操作の実行を必要とする場合、それらのいずれかを開始する前にすべてが成功する可能性が高いかどうかを確認する方が、ロールバック必要なアクションを実行するよりも優れていることがよくあります。初期チェックを行うことで、ロールバックが必要になるすべての状況を回避できるわけではありませんが、そのような場合の頻度を減らすのに役立つ可能性があります。
supercat

38

あなたが「早く失敗する」と呼ぶものと私がそれを呼ぶものは同じではないと思います。

データベースに変更を行うように指示し、障害を処理します。これは高速です。あなたの方法は複雑で、遅く、特に信頼できるものではありません。

あなたのテクニックは早く失敗するのではなく、「プリフライト」です。正当な理由がある場合もありますが、データベースを使用する場合はそうではありません。


1
あるクラスが別のクラスに依存しているときに2番目のクエリが必要な場合があるため、そのような場合には選択肢がありません。
コンラッド

4
しかし、ここではありません。また、データベースクエリは非常に賢い場合があるため、一般に「選択の余地がない」ことには疑問があります。
gnasher729

1
また、アプリケーションにも依存すると思います。数人のユーザーのためだけに作成した場合、違いは生じず、2つのクエリでコードが読みやすくなります。
コンラッド

21
DBが一貫性のないデータを格納していると仮定しています。言い換えれば、DBとデータの一貫性を信頼していないようです。もしそうなら、あなたは本当に大きな問題を抱えており、あなたの解決策はウォークアラウンドです。緩和的解決策は運命づけられ、遅かれ早かれ却下された。制御と管理からDBを消費せざるを得ない場合があります。他のアプリケーションから。そのような場合、そのような検証を検討します。いずれにせよ、@ gnasherは正しいです。あなたのものはすぐに失敗するわけではありません。
ライヴ

15

これはコメントとして始まりましたが、大きくなりすぎました。

いいえ、他の回答が述べているように、このパターンは使用すべきではありません。*

非同期コンポーネントを使用するシステムを扱う場合、チェックと変更の間でデータベース(またはファイルシステム、または他の非同期システム)が変更される可能性のある競合状態が常に存在します。このタイプのチェックは、処理したくないエラーのタイプを防ぐ信頼できる方法ではありません。
十分でないよりも悪い、一目でそれがあるという印象与えなければならない誤った安心感を与え、重複レコードのエラーを回避します。

とにかくエラー処理が必要です。

コメントでは、複数のソースからのデータが必要かどうかを尋ねました。
まだ

根本的な問題は、あなたがチェックしたいことは、より複雑になると消えません。

とにかくエラー処理が必要です。

このチェックが、保護しようとしている特定のエラーを防ぐ信頼できる方法であったとしても、他のエラーが発生する可能性があります。データベースへの接続が失われたり、スペースが不足したりするとどうなりますか?

とにかく、まだ他のデータベース関連のエラー処理が必要です。この特定のエラーの処理は、おそらくそれの小さな部分でなければなりません。

変更するものを決定するためにデータが必要な場合は、明らかにどこかから収集する必要があります。(使用しているツールによっては、個別のクエリを収集するよりもおそらく良い方法があります)収集したデータを調べる際に、結局変更を加える必要がないと判断した場合、変化する。この決定は、エラー処理の問題とはまったく別のものです。

とにかくエラー処理が必要です。

繰り返していることは知っていますが、これを明確にすることが重要だと感じています。私は前にこの混乱を一掃しました。

最終的には失敗します。失敗すると、最後まで到達するのが難しく、時間がかかります。競合状態から生じる問題を解決するのは困難です。それらは一貫して発生しないため、単独で再現することは困難であるか、不可能ですらあります。最初は適切なエラー処理を行っていなかったので、先に進むことはあまりないでしょう。おそらく、エンドユーザーのいくつかの不可解なテキストのレポート(そもそも見ないようにしようとしていたのでしょう)。おそらく、その関数を参照するスタックトレースは、その関数を見ると、エラーを露骨に否定する可能性さえあります。

*これらの存在チェックを実行する正当なビジネス上の理由があるかもしれません。例えば、アプリケーションが高価な作業を複製するのを防ぐためなどですが、適切なエラー処理の適切な代替ではありません。


2

ここで注意すべき副次的なことを考えます-これが必要な理由の1つは、ユーザーに表示されるエラーメッセージをフォーマットできるようにするためです。

私は心からお勧めします:

a)発生するすべてのエラーに対して、同じ一般的なエラーメッセージをエンドユーザー表示します。

b)実際の例外を、開発者のみがアクセスできる場所(サーバー上)またはエラー報告ツールから送信できる場所(クライアントが展開されている場合)に記録します。

c)より有用な情報を追加できない限り、記録するエラー例外の詳細をフォーマットしようとしないでください。問題を追跡するために使用できたはずの有用な情報を誤って「フォーマット」してしまいたくないのです。


つまり、例外には非常に役立つ技術情報がたくさんあります。これはエンドユーザー向けではなく、危険を冒してこの情報を失います。


2
「発生するすべてのエラーについて、同じ一般的なエラーメッセージをエンドユーザーに表示します。」それは..行うには恐ろしいことのようにエンドユーザーのルックスの例外を書式設定、主な理由だった
コンラッド・

1
合理的なデータベースシステムであれば、プログラムで何かが失敗した理由を見つけることができるはずです。例外メッセージを解析する必要はありません。より一般的には、エラーメッセージをユーザーに表示する必要があると誰が言うのでしょうか。最初の挿入に失敗し、成功するまで(または再試行または時間の制限まで)ループで再試行できます。実際、バックオフと再試行はいずれにせよ最終的に実装したいものです。
ダニエルプライデン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.