データベースからの誤ったnullエントリを防ぐための設計と実践


9

私のプログラムの一部は、データベース内の多くのテーブルと列からデータをフェッチして処理します。一部の列はである可能性がありますがnull、現在の処理コンテキストではエラーです。

これは「理論的には」発生しないはずなので、発生する場合は、不良データまたはコード内のバグを示しています。エラーの重大度は、フィールドによって異なりnullます。つまり、一部のフィールドでは処理を停止して誰かに通知する必要があり、他のフィールドでは処理を続行して誰かに通知するだけにする必要があります。

まれですが可能なnullエントリを処理するための優れたアーキテクチャまたは設計原則はありますか?

ソリューションはJavaで実装できるはずですが、問題は言語にとらわれないため、タグを使用しませんでした。


私自身が持っていたいくつかの考え:

NOT NULLの使用

最も簡単なのは、データベースでNOT NULL制約を使用することです。

しかし、データの元の挿入がこの後の処理ステップよりも重要である場合はどうなりますか?そのため、挿入がnull(バグまたは何らかの正当な理由のために)テーブルに挿入される場合、挿入が失敗しないようにします。プログラムのさらに多くの部分が挿入されたデータに依存しているが、この特定の列には依存していないとしましょう。そのため、挿入ステップではなく、現在の処理ステップでエラーが発生する危険を冒したいのです。それが、NOT NULL制約を使用したくない理由です。

単純にNullPointerExceptionに依存

常にそこにあると期待しているかのようにデータを使用し(実際にそうであるはずです)、結果のNPEを適切なレベルでキャッチします(たとえば、現在のエントリの処理は停止しますが、処理全体は進行しません) )。これは「フェイルファースト」の原則であり、私はしばしばそれを好みます。少なくともバグの場合、ログに記録されたNPEを取得します。

しかしその後、さまざまな種類の欠落データを区別する能力が失われます。たとえば、一部の欠落しているデータについては除外することができますが、他の場合は処理を停止して管理者に通知する必要があります。

null各アクセスの前に確認し、カスタム例外をスローする

カスタム例外を使用すると、例外に基づいて正しいアクションを決定できるため、これは進むべき道のようです。

しかし、どこかで確認するのを忘れた場合はどうなりますか?また、私はコードを、まったくまたはほとんど期待されない(そしてビジネスロジックフローの一部ではない)nullチェックで混乱させます。

この方法を選択した場合、どのパターンがアプローチに最適ですか?


私のアプローチについての考えやコメントは大歓迎です。また、あらゆる種類の優れたソリューション(パターン、原則、私のコードまたはモデルの優れたアーキテクチャなど)。

編集:

別の制約があります。ORMを使用してDBから永続オブジェクトへのマッピングを行うため、そのレベルでnullチェックを実行しても機能しません(nullが害を及ぼさない部分で同じオブジェクトが使用されるため)。 。これまでに提供された回答の両方がこのオプションについて言及したため、これを追加しました。


5
「一部の列はnullである可能性がありますが、現在の処理コンテキストではエラーになります。...挿入によってnullがテーブルに挿入される場合、挿入が失敗しないようにしてください。」これら2つの要件は矛盾。それはです不可能あなたが2つの条件のいずれかをリラックスするまで、解決策を見つけるために。
Kilian Foth、2016年

@KilianFothまあ、私の緩和は、「現在の処理」コンテキストのエラーは挿入時よりも深刻ではないということです。したがって、私はまれな処理エラーを受け入れますが、それらを処理するための優れた堅牢な設計を望んでいます。だからこそ、NOT NULLは、そうでなければ良い解決策ですが、ここでは不可能です。
jhyot

1
非常に多くのエラーを受け入れようとする場合、そのエラーの発信者はそれらを修正することは決してありません。厄介な挿入ステートメントが成功した場合、問題を修正するためにどのようなインセンティブが必要になりますか?堅牢ではなく、不良データを受け入れると考えていますか?
TulainsCórdova16年

@ user61852私は明示的にエラーを受け入れていませんが、それらを適切に処理したいと思っています。nullポインタを飲み込むことは問題外です。また、挿入が成功する必要があるが、この特定のフィールドを設定する必要がない他の多くの部分よりも、私の部分が本当に(ビジネスで定義されているように)客観的に重要でない場合はどうなりますか?挿入は、値を追加するように強制できるユーザーエントリからではなく、省略がバグである可能性が高い他のコードから発生します(ただし、挿入を壊すほど重要ではありません)。
jhyot

1
データベースでそれらをNOT NULLとしてマークするのが最良の解決策です。列がnull可能である場合、ストレージメカニズムで許可されているため、予期されていない場合でも、コードはその場合を処理する必要があります。
Jon Raynor

回答:


9

結果セットからオブジェクトを構築するマッピングコードにnullチェックを配置します。これにより、チェックが1か所に配置され、エラーが発生する前にレコードの処理の途中でコードを実行できなくなります。アプリケーションフローのしくみによっては、各レコードを一度に1つずつマッピングして処理するのではなく、すべての結果のマッピングを前処理ステップとして実行したい場合があります。

ORMを使用している場合は、各レコードを処理する前にすべてのnullチェックを実行する必要があります。recordIsValid(recordData)-typeメソッドをお勧めします。これにより、すべてのnullチェックと他の検証ロジックを(もう一度)1か所に保持できます。ヌルチェックを残りの処理ロジックと混在させないでください。


ありがとう、それは良い洞察です!私は確かにORMを使用しているので、そのレベルのチェックは機能しません。しかし、永続オブジェクトから実際のドメインオブジェクトへのマッピングもいくつかあります。前処理ステップでマッピングと検証が可能かどうかを確認します。
jhyot

ORMを切り替えると、どうなるでしょうか。これをソースで防御する方が良い(Doc Brownの回答を参照)。
ロビーディー

@RobbieDee:関係ありません。マッピングコードを書き換える必要がある場合は、nullチェックがそこにあり、書き換えの一部としてそれらを変更するか、ビジネスオブジェクトでnullチェックを実行する別のメソッドがあるため、書き換えは必要ありません。そして、Doc Brownが示唆しているように、デフォルト値でその事実を確認するのではなく、データが欠落していることに気づくことが重要な場合があります。
TMN 2016年

これは、ETLフローの上位で発生するはずです。この方法では、作業が重複するリスクがあります。
ロビーディー

6

nullの挿入はエラーのようですが、データを失いたくないので、挿入時にこのエラーを強制することを恐れます。ただし、フィールドがnullであってはならないのにnullである場合、データが失われます。したがって、最善の解決策は、最初にnullフィールドが誤って保存されないようにすることです。

このためには、そのデータの1つの信頼できる永続的なリポジトリであるデータベースでデータが正しいことを強制します。nullではない制約を追加することにより、これを行います。次に、コードは失敗する可能性がありますが、これらの失敗はすぐにバグを通知し、すでにデータを失う原因となっている問題を修正できます。バグを簡単に特定できるようになったので、コードをテストして2回テストします。nullを気にする必要がないため、データの損失につながるバグを修正し、その過程で、データのダウンストリーム処理を大幅に簡略化できます。


2
答えてくれてありがとう。あなたの解決策はそれを行う正しい方法であり、あなたはそれを簡潔に言いました。私の影響範囲外の制約により、困難または不可能になる可能性があります(たとえば、テスト用のリソースを利用できない、または既存のコードを自動的にテスト可能にするため)。他の方法を試す前に、このソリューションが機能するかどうかを確認する必要があります。私の当初の考えでは、私は多分あまりに早く想定しすぎて、問題の原因を修正できないと思いました。
jhyot

@jhyotわかりました。あなたが物事をきれいな方法で行うことができないとき、それはイライラします。うまくいけば、私の答えは、同様の問題を抱えているが、事後の混乱を片付けるのではなく根本原因を攻撃することができる他の人にとって少なくとも有用です。
モニカ

5

質問のこの文に関して:

これは「理論的には」発生しないはずなので、発生する場合は、不良データまたはコード内のバグを示しています。

私は常にこの引用に感謝しています(この記事の厚意により):

初心者プログラマーが彼らの主な仕事がプログラムのクラッシュを防ぐことであると信じているとき、それは面白いと思います。この壮大な失敗の議論は、そのようなプログラマにとってはそれほど魅力的ではないでしょう。より経験豊富なプログラマーは、正しいコードが優れていること、クラッシュするコードが改善をもたらす可能性があることを認識していますが、クラッシュしない誤ったコードは恐ろしい悪夢です。

基本的に、ポステルの法則を承認しているように聞こえます。「送信するものは保守的に、受け入れるものは寛大に」。理論的に優れていますが、実際には、この「堅牢性の原則」は、少なくとも長期的には、そして場合によっては短期的にも、堅牢ないソフトウェアにつながります。(主にネットワークプロトコルのユースケースに重点を置いていますが、Eric Allmanの論文「The Robustness Principle Reconsidered」と比較してください。

データベースにデータを誤って挿入するプログラムがある場合、それらのプログラムは壊れており、修正する必要があります。問題を取り上げると、悪化し続けるだけです。これは、常習者が依存症を継続できるようにするソフトウェアエンジニアリングに相当します。

ただし、実際には、少なくとも一時的に、特に緩やかな壊れた状態から厳密な正しい状態へのシームレスな移行の一部として、「壊れた」動作を続行できるようにする必要がある場合があります。その場合、誤った挿入を成功せる方法を見つけながら、「正規の」データストアを常に正しい状態にすることができます。これにはさまざまな方法があります。

  • データベーストリガーを使用して、不正な挿入を正しい挿入に変換します。たとえば、欠損値またはnull値をデフォルトに置き換えます。
  • 正しくないプログラムが、「正しくない」ことが許可されている別個のデータベーステーブルに挿入し、修正されたデータをそのテーブルから正規データストアに移動する別のスケジュールされたプロセスまたはその他のメカニズムを用意する
  • クエリ側のフィルタリング(ビューなど)を使用して、データベースから取得したデータが常に正しい状態であることを確認します。

これらすべての問題を回避する1つの方法は、書き込みを発行するプログラムと実際のデータベースの間に制御するAPIレイヤー挿入することです

問題の一部のように思われますが、正しくない書き込みを生成しているすべての場所がわからない、または更新するにはそれらの場所が多すぎます。それは恐ろしい状態ですが、そもそもそれが発生することは許されるべきではありませんでした。

正規の本番データストアのデータを変更できるシステムがいくつかあると、すぐに問題が発生します。そのデータベースについて何かを集中管理する方法はありません。できる限り少ないプロセスに書き込みを許可し、必要に応じて挿入する前にデータを前処理できる「ゲートキーパー」としてそれらを使用することをお勧めします。このための正確なメカニズムは、実際には特定のアーキテクチャによって異なります。


「データベースにデータを誤って挿入するプログラムがある場合、それらのプログラムは壊れており、修正する必要があります。」これは理論的にもすばらしいことですが、実際には、レコードを追加し続ける一方で、一部の委員会が「NA」と「None」のどちらを使用するかについて議論を続けています。
JeffO 2016年

@JeffO:「NA」、「None」、NULL、またはその他の何かをデータベースに格納するかどうかを議論する委員会はありません。非技術的な利害関係者は、データベースから取得されるデータとその使用方法に関係がありますが、内部表現には関係ありません。
Daniel Pryden

@DanielPryden:私の最後の仕事では、クロスドメインの技術的な変更をレビューするアーキテクチャレビューボード(DBA小委員会)がいました。非常に技術的ですが、彼らは2週間ごとに会っただけで、彼らに十分な詳細を提供しなかった場合、彼らはあなたがそうするまで決定を延期します...その後の会議で。新しいコードによる機能の追加で構成されなかった重要なシステム変更のほとんどは、通常1か月ほどかかります。
TMN

@DanielPryden-私は、テキストボックスラベルについての上級管理者による討論の会議に参加しました。これは、アプリケーションまたはデータベースで名前を付けることとは何の関係もないと主張することができますが、そうではありません。
JeffO 2016年

この種の変更について追加の承認を取得することについてのコメントへの応答:値が「正しくない」という私の指摘は、許容値がすでにどこかに文書化されていることを前提としています。そのため、OPはこれらの値をバグと見なす必要があると述べています。データベースのスキーマが値を許可するように指定されている場合、その値はバグではありません。重要なのは、スキーマと一致しないデータがある場合、何かが壊れているということです。優先度は、データとスキーマを一致させることです。チームによっては、データ、スキーマ、またはその両方を変更する必要があります。
Daniel Pryden 2016年

2

まれに起こり得るnullエントリを処理するための優れたアーキテクチャまたは設計原則はありますか?

簡単な答え-はい。

ETL

データがデータベースに入るのに十分な品質であることを確認するために、いくつかの事前処理を実行します。ドロップファイルの内容はすべて報告され、クリーンなデータをデータベースにロードできます。

密猟者(開発者)とゲームキーパー(DBA)の両方である人物として、私は苦い経験から、第三者が強制されない限り、データの問題を解決しないことを知っています。常に後ろ向きに曲がり、データをマッサージすることは危険な先例を設定します。

マート/リポジトリ

このシナリオでは、生データがリポジトリDBにプッシュされ、アプリケーションがアクセスできるマートDBにサニタイズされたバージョンがプッシュされます。

デフォルト値

列に適切なデフォルト値を適用できる場合は、これが必要ですが、これが既存のデータベースの場合は、多少の作業が必要になる場合があります。

早く失敗する

アプリケーション、レポートスイート、インターフェイスなどへのゲートウェイでデータの問題に対処するのは魅力的です。これだけに依存しないように強くお勧めします。他のウィジェットをDBに接続すると、同じ問題に直面する可能性があります。データ品質の問題に対処します。


+1これは私がやろうとしていることであり、すべてのデータを収集し、アプリケーションが処理する有効なデータセットを作成します。
Kwebble 2016年

1

ユースケースでNULLを適切なデフォルト値で安全に置き換えることができる場合はいつでも、またはSELECTを使用してSqlステートメントで変換を行うことができます。だから代わりにISNULLCOALESCE

 SELECT MyColumn FROM MyTable

書ける

 SELECT ISNULL(MyColumn,DefaultValueForMyColumn) FROM MyTable

もちろん、これはORMが選択ステートメントを直接操作できる場合、または生成用に変更可能なテンプレートを提供できる場合にのみ機能します。この方法で「実際の」エラーがマスクされていないことを確認する必要があります。そのため、デフォルト値による置換がNULLの場合に必要なものである場合にのみ適用してください。

データベースとスキーマを変更でき、dbシステムがこれをサポートしている場合は、@ RobbieDeeで提案されているように、特定の列にデフォルト値句を追加することを検討できます。ただし、これにより、データベース内の既存のデータを変更して以前に挿入されたNULL値を削除する必要があり、正しいインポートデータと不完全なインポートデータを後で区別する機能が削除されます。

私自身の経験から、ISNULLを使用すると驚くほどうまくいくことがわかっています-以前は、元の開発者がNOT NULL制約を多数の列に追加するのを忘れていたレガシーアプリケーションを維持する必要があり、後でこれらの制約を簡単に追加することはできませんでしたいくつかの理由で。しかし、すべてのケースの99%で、数値列のデフォルトとして0を、テキスト列のデフォルトとして空の文字列を完全に受け入れました。


これが機能している間は、SELECTごとに防御コードを複製しなければならない場合があります。はるかに優れたアプローチは、NULLが挿入されたときに列のデフォルト値を定義することですが、これはさまざまな理由で可能でない/望ましくない場合があります。
ロビーディー

@RobbieDee:その発言に感謝し、それに応じて私の答えを変更しました。ただし、これが「はるかに良い」かどうかは議論の余地があります。CRUDコードが1か所にある場合、防御コードの重複はそれほど問題にならない場合があります。そうでない場合は、すでにいくつかのコードの重複が事前に発生しています。
Doc Brown

単純なCRUD操作はもちろん理想的です。しかし、現実の世界では、システムには多くの場合、複雑なUIビュー、ユーザー生成のデータウィザード、レポートなどがあります。ブラウンフィールド開発では、あなたが説明したことが望ましいかもしれません。
ロビーディー

ベストアンサー。新しいアプリケーションは通常、あなたのコントロールの外にあるかもしれないいくつかの新しいデータを追加します。通常、誤ったNULLは、レガシーデータを再設計されたデータベースにインポートすることで発生します。数日ではなく数時間で完了できるように、これに対する制約はオフになっています。「大きな失敗」は、DBAが制約を再度有効にしようとしたときにしばしば発生します。それが計画されたことは一度もなかったので、経営者は悪いデータを修正するためにしばしば必要とされる何週間もの仕事に手を出すので、それは残っています。すべてのアプリは、デフォルトを挿入し、それ以外の場合は欠落しているデータを報告またはプロンプトすることにより、NULLを適切に処理する必要があります。
DocSalvager

1

OPは、ビジネスルールとデータベースの技術的な詳細を結び付ける回答を想定しています。

これは「理論的には」発生しないはずなので、発生する場合は、不良データまたはコード内のバグを示しています。どのフィールドがnullであるかによって、エラーの重大度は異なります。つまり、一部のフィールドでは処理を停止して誰かに通知する必要があり、他のフィールドでは処理を続行して誰かに通知するだけにする必要があります。

これはすべてのビジネスルールです。ビジネスルールは、それ自体がnullであることを気にしません。データベースがnull、9999、 "BOO!"になる可能性があることはすべて知っています。...それは単なる別の値です。つまり、RDBMSでは、nullには興味深いプロパティがあり、ユニークな使用法には意味がありません。

重要なのは、特定のビジネスオブジェクトの「null性」が何を意味するかだけです...

まれに起こり得るnullエントリを処理するための優れたアーキテクチャまたは設計原則はありますか?

はい。

  • ビジネスルールをクラスに入れます。
  • 文字変換は、ビジネスクラスとデータストアを分離する適切なコードレイヤーで行う必要があります。ORMコードに配置できない場合は、少なくともデータベースに配置しないでください。
  • ここでは、ビジネスルールはなく、データベースをできるだけダムにしてください。デフォルト値のような無害なことでさえ、あなたに噛み付くでしょう。行ったことがある。
  • データベースとの間でやり取りされるデータを検証します。そしてもちろん、これはビジネスオブジェクトのコンテキスト内で行われます。

データ取得時に例外をスローしても意味がありません。

問題は「「悪い」データを保存すべきか」ということです。場合によります:

  • 不正なデータが使用されている可能性があります -無効なオブジェクトまたはオブジェクトの複合を保存しないでください。あらゆる場所で複雑なデータ/ビジネス関係。ユーザーはいつでも任意の機能を実行でき、おそらくそのビジネスエンティティをさまざまなコンテキストで使用できます。保存時の不良データの影響(存在する場合)は、将来の使用に大きく依存するため不明です。そのデータの統一された/単一のプロセスはありません。
  • 不良データがある場合は進行できません - 不良データの保存を許可します。ただし、プロセスの次のステップは、すべてが有効になるまで続行できません。たとえば、所得税を行う。データベースから取得されると、ソフトウェアはエラーを指摘し、有効性の確認なしにIRSに送信することはできません。

0

nullを処理する方法はたくさんあるので、データベースレイヤーからアプリケーションレイヤーに移ります。


データベース層

null禁止できます。ここではそれは非現実的ですが。

列ごとにデフォルト設定できます

  • そのカラムがあることを必要と存在しないからinsert、それほど明示的ヌル挿入をカバーしていません
  • insert誤ってこの列を逃した行からの検出を防ぎます

トリガー設定して、挿入時に欠損値が自動的に計算されるようにすることができます

  • この計算を実行するために必要な情報が存在する必要があります
  • それは遅くなります insert

クエリ層

不便があるスキップできますnull

  • メインロジックを簡素化します
  • 「不良行」の検出を防ぐため、それらを確認するには別のプロセスが必要になります
  • 各クエリをインストルメント化する必要があります

クエリにデフォルト値指定できます。

  • メインロジックを簡素化します
  • 「不良行」の検出を防ぐため、それらを確認するには別のプロセスが必要になります
  • 各クエリをインストルメント化する必要があります

注:自動生成された方法でクエリを生成する場合、各クエリのインストルメントは必ずしも問題ではありません。


アプリケーション層

テーブルの禁止を事前にチェックできますnull

  • メインロジックを簡素化します
  • 故障までの時間を改善します
  • 事前チェックとアプリケーションロジックの一貫性を保つ必要があります

禁止されている場合は、処理中断できますnull

  • どの列が可能でどの列nullが不可能であるかについての知識の重複を回避します
  • それはまだ比較的単純です(チェック+リターン/スローのみ)
  • プロセスを再開可能にする必要があります(すでに電子メールを送信している場合は、2度または100度送信したくない!)

禁止されている場合は、行スキップできますnull

  • どの列が可能でどの列nullが不可能であるかについての知識の重複を回避します
  • それはまだ比較的単純です(チェック+リターン/スローのみ)
  • プロセスが再開可能である必要はありません

禁止されているに遭遇したときに、一度に1つずつ、またはバッチで通知送信できますnull。これは、上記の他の方法を補完するものです。ただし、最も重要なのは「次に何をするか」です。特に、行にパッチが適用され、再処理が必要な場合は、すでに処理済みの行と必要な行を区別する方法があることを確認する必要があります。再処理されています。


あなたの状況を考慮して、私はアプリケーションで状況を処理し、次のいずれかを組み合わせます:

  • 中断して通知する
  • スキップして通知

特に処理に時間がかかる場合は、ある程度の進歩を保証するために、可能であればスキップする傾向があります。

スキップされた行を再処理する必要がない場合は、それらをログに記録するだけで十分であり、スキップされた行の数をプロセスの最後に送信する電子メールは適切な通知になります。

それ以外の場合は、行を修正(および再処理)するためにサイドテーブルを使用します。このサイドテーブルは、単純な参照(外部キーなし)または完全なコピーのいずれかですnull。メインデータをクリーンアップする前にに対処する時間がない場合、後者は(たとえより高価であっても)必要です。


-1

nullは、データベースタイプから言語タイプへの変換またはマッピングで処理できます。たとえばC#の場合、任意の型のnullを処理する汎用メソッドは次のとおりです。

public static T Convert<T>(object obj)
        {
            if (obj == DBNull.Value)
            {
                return default(T);
            }

            return (T) obj;
        }

public static T Convert<T>(object obj, T defaultValue)
        {
            if (obj == DBNull.Value)
            {
                T t = defaultValue;
                return t;
            }

            return (T) obj;
        }

または、アクションを実行する場合...

 public static T Convert<T>(object obj, T defaultValue)
        {
            if (obj == DBNull.Value)
            {
                //Send an Alert, we might want pass in the name
                //of column or other details as well
                SendNullAlert();
                //Set it to default so we can keep processing
                T t = defaultValue;
                return t;
            }

            return (T) obj;
        }

次に、マッピングでは、この場合は「Sample」タイプのオブジェクトに、任意の列のnullを処理します。

public class SampleMapper : MapperBase<Sample>
    {
        private const string Id = "Id";
        private const string Name = "Name";
        private const string DataValue = "DataValue";
        private const string Created = "Created";

        protected override Sample Map(IDataRecord record)
        {
            return new Sample(
                Utility.Convert<Int64>(record[Id]),
                Utility.Convert<String>(record[Name]),
                Utility.Convert<Int32>(record[DataValue]),
                Utility.Convert<DateTime>(record[Created])
                );
        }
    }

最後に、SQLデータ型を調べて言語固有のデータ型に変換することにより、関連するSQLクエリまたはテーブルに基づいて、すべてのマッピングクラスを自動的に生成できます。これは、多くのORMが自動的に行うことです。一部のデータベースタイプには直接マッピングがない場合があり(地理空間列など)、特別な処理が必要になる場合があります。


誰かが...素晴らしいことだ同等のJavaのバージョンを投稿したい場合
ジョンRaynor

サンプルコードは、Java開発者にとっても完全に理解できると思います。私の状況では、ORMが既に配置されているので、ORMを実装する必要はありません。しかし、あなたの答えはnullのデフォルト値のみを扱いますが、私の場合、実際にははるかに重要なケースはnullを検出してアクションをトリガーすることです(たとえば、誤ったデータについて管理者に通知します)。
jhyot

ああ、これに基づいて私の答えを更新します。
Jon Raynor

編集したコードには、すべてのnull値に対するデフォルトのアクションが1つあります(つまり、完全に汎用的です)。これは、元の質問の2番目のオプションと非常に似ています。つまり、nullをスローしてどこかでキャッチします。しかし、そこに述べられているように、欠けている値に基づいてアクションを区別する必要があります。
jhyot
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.