web-dev-qa-db-ja.com

私はデータベースに何かが存在するかどうかを確認し、すぐに失敗するか、データベースの例外を待つべきですか?

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-net-mvc-application#modify-the-department-controller ですが、SaveChangesには追加の例外処理があります

また、このチェックの主な目的は、わかりやすいメッセージと既知のHTTPステータスをAPIのユーザーに返すことであり、データベースの例外を完全に無視しないことでした。そして、スローされる唯一の場所の例外はSaveChangesまたはSaveChangesAsync呼び出しの内側です...したがって、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

32
Konrad

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

まず第一に、あなたの例ではあなたはデータレイヤーにいて、SQLを実行するためにデータベースで直接EFを使用しています。あなたのコードは実行と同等です

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にあるかどうかがはっきりしない状況を想像するのは困難です。チェックはおかしいでしょう。しかし、ユーザーが入力した自然なキーである場合はどうでしょうか。それは存在しない可能性が高い

4
Ewan

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

111
Kilian Foth

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

データベースに変更を加えて失敗を処理するように指示すると、それが高速になります。あなたの方法は複雑で遅く、特に信頼性が高くありません。

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

38
gnasher729

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

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

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

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

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

根本的な問題は、チェックしたいものがより複雑になっても解消されません。

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

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

とにかく、まだotherデータベース関連のエラー処理が必要です。この特定のエラーの処理は、おそらくそれの小さな部分であるべきです。

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

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

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

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

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

15
Mr.Mindor

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

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

a)発生したeveryエラーの一般的なエラーメッセージをエンドユーザーに表示します。

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

c)役立つ情報を追加できない限り、ログに記録するエラー例外の詳細をフォーマットしないでください。問題を追跡するために使用できたであろう1つの有用な情報を誤って「フォーマット」したくない場合。


つまり、例外には非常に役立つ技術情報が満載です。これはエンドユーザー向けではなく、危険にさらされたときにこの情報を失うことになります。

2
Paddy