防范数据库中错误的空条目的设计和实践


9

程序的一部分从数据库中的许多表和列中获取数据进行处理。有些列可能是null,但是在当前处理上下文中这是一个错误。

从理论上讲,这不会发生,因此,如果这样做,则表明数据错误或代码中存在错误。错误具有不同的严重性,具体取决于哪个字段null。也就是说,对于某些字段,应停止处理并通知某人;对于其他字段,应允许该处理继续进行,而仅通知某人。

是否有任何好的架构或设计原则来处理稀有但可能的null条目?

解决方案应该可以用Java来实现,但是我没有使用标记,因为我认为问题在某种程度上与语言无关。


我的一些想法:

使用NOT NULL

最简单的方法是在数据库中使用NOT NULL约束。

但是,如果原始数据插入比随后的处理步骤更重要,该怎么办?因此,如果插入内容将a null放入表中(由于错误或什至出于某些合理的原因),我不希望插入操作失败。假设程序的更多部分取决于插入的数据,但不取决于此特定列。因此,我宁愿冒险在当前处理步骤而不是插入步骤中出错。这就是为什么我不想使用NOT NULL约束的原因。

天真的取决于NullPointerException

我可以使用数据,就像我期望它始终存在一样(这确实应该如此),并在适当的级别上捕获生成的NPE(例如,以便停止当前条目的处理,而不是整个处理进度) )。这是“快速失败”的原则,我经常喜欢它。如果这是一个错误,至少我会得到一个已记录的NPE。

但是后来我失去了区分各种丢失数据的能力。例如,对于某些丢失的数据,我可以将其省略,但是对于其他一些数据,应停止处理并通知管理员。

null在每次访问之前进行检查并引发自定义异常

自定义异常可以让我根据异常来决定正确的操作,因此这似乎是可行的方法。

但是,如果我忘记在某个地方检查该怎么办?然后,我还会使用从未有过或很少有期望的空检查(因此绝对不是业务逻辑流程的一部分)使我的代码混乱。

如果我选择这种方式,哪种模式最适合该方法?


欢迎对我的方法提出任何想法和意见。还有任何更好的解决方案(模式,原理,代码或模型的更好体系结构等)。

编辑:

还有一个约束,因为我使用ORM进行从DB到持久性对象的映射,因此在该级别上执行null检查将不起作用(因为在null不会造成任何危害的部分中使用了相同的对象) 。我添加此内容是因为到目前为止提供的答案都提到了该选项。


5
“某些列可能为空,但是在当前处理上下文中,这是一个错误。...如果插入会将空值放入表中,则我不希望插入失败。”这两个要求是矛盾的。这是不可能的,直到你放松的两个条件之一,以找到一个解决方案。
凯莉安

@KilianFoth好吧,我的放松是,“当前处理”上下文中的错误不如插入时严重。因此,我接受罕见的处理错误,但是我希望有一个很好的健壮设计来处理它们。这就是为什么NOT NULL(否则将是一个很好的解决方案)在这里不可能的原因。
jhyot

1
如果您要接受这么多错误,那么错误的始发者将永远不会纠正它们。如果他们凌乱的插入语句成功,那么他们有什么动力来解决问题?您是否认为健壮而不失败但可以接受不良数据?
TulainsCórdova16年

@ user61852我明确地不接受错误,但是想要优雅地处理它们。吞没空指针是不可能的。另外,如果我的部分在客观上(由业务部门定义)确实不如许多其他需要插入成功但不需要设置此特定字段的部分重要,该怎么办?插入不是源自用户可以强迫他们添加值的用户条目,而是源自其他代码,在这些代码中,遗漏很可能是一个错误(但不足以破坏插入)。
jhyot

1
在数据库中将它们标记为NOT NULL是最好的解决方案,如果列可以为空,那么即使存储模块允许它是不可预期的,代码也需要处理这种情况。
乔恩·雷诺

Answers:


9

我会将空检查放在映射代码中,从结果集中构建对象。这将检查放在一个地方,并且不会让您的代码在遇到错误之前就无法处理记录。根据应用程序流程的工作方式,您可能希望将所有结果映射为一个预处理步骤,而不是一次映射和处理每个记录。

如果您使用的是ORM,则必须在处理每个记录之前执行所有空检查。我建议使用recordIsValid(recordData)-type方法,这样,您可以(再次)将所有null检查和其他验证逻辑放在一个地方。我绝对不会将null检查与其余处理逻辑混合在一起。


谢谢,这是很好的见解!我确实使用的是ORM,因此无法进行该级别的检查。但是我也有一些从持久性对象到真实域对象的映射。我将检查是否可以在预处理步骤中进行映射和验证。
jhyot

如果您切换ORM,那又如何呢?最好从源头上对此进行辩护(请参阅Doc Brown的回答)。
罗比·迪

@RobbieDee:没关系。如果必须重写映射代码,则可以在其中进行空检查,然后在重写过程中对其进行修改,或者可以使用单独的方法对业务对象执行空检查,因此无需进行重写。正如Doc Brown所暗示的那样,有时很重要的一点是要注意丢失数据,而不是用默认值掩盖事实。
TMN

这应该在ETL流中进一步发生。您仍然会有这种方式重复工作的风险。
罗比·迪

6

听起来像插入null是一个错误,但是您害怕在插入时强制执行此错误,因为您不想丢失数据。但是,如果一个字段不应该为null而是为null,则您将丢失data。因此,最佳解决方案是确保首先不要错误地保存空字段。

为此,请在该数据的一个权威性永久性存储库(数据库)中确保数据正确。通过添加非空约束来做到这一点。然后,您的代码可能会失败,但是这些失败会立即将错误通知您,从而使您可以纠正已经导致丢失数据的问题。现在,您可以轻松地识别错误,测试代码并对其进行两次测试。您将能够纠正导致数据丢失的错误,并在此过程中极大地简化了数据的下游处理,因为您无需担心null。


2
感谢您的回答。我同意您的解决方案是实现此目标的正确方法,并且您已简洁地表述了它。我无法控制的约束可能使它变得困难或不可能(例如,没有可用的测试资源或用于使现有代码可自动测试的资源),但我绝对应该在尝试其他方法之前仔细检查此解决方案是否可以工作。在我最初的想法中,我可能过快地假设我无法从根本上解决问题。
jhyot's

@jhyot好的。当您不能以整洁的方式做事时,这令人沮丧。希望我的回答至少对遇到类似问题但能够解决根本原因而不是在事后清理混乱的其他人有用。
恢复莫妮卡

5

关于这个问题的句子:

从理论上讲,这应该不会发生,因此,如果这样做,则表明数据错误或代码中存在错误。

我一直很欣赏这句话(本文提供):

当新手程序员认为他们的主要工作是防止程序崩溃时,我感到很有趣。我想象这种引人注目的失败论点对这样的程序员不会那么有吸引力。更有经验的程序员意识到正确的代码很棒,崩溃的代码可以使用改进,但是不崩溃的错误代码是一个可怕的噩梦。

基本上:听起来您是在赞同Postel法则,“对发送的邮件要保守,对接受的邮件要宽松”。虽然理论上很棒,但实际上,这种“健壮性原则”导致软件至少在长期(有时甚至在短期内健壮。(比较Eric Allman的论文《重新考虑稳健性原则》,它是对该主题的非常详尽的论述,尽管它主要关注网络协议用例。)

如果您有错误地将数据插入数据库的程序,则这些程序已损坏,需要修复。用纸贴住问题只会使情况继续恶化;这相当于使瘾君子继续沉迷的软件工程。

但是,从务实的角度来讲,有时确实需要使“中断”行为至少暂时地继续,特别是作为从松懈,中断状态到严格,正确状态的无缝过渡的一部分。在那种情况下,您想找到一种方法来允许不正确的插入成功,但仍然允许“规范”数据存储始终处于正确的状态。有多种方法可以做到这一点:

  • 使用数据库触发器将格式错误的插入内容转换为正确的插入内容,例如通过将缺失/空值替换为默认值
  • 将不正确的程序插入允许“不正确”的单独数据库表中,并具有单独的计划进程或其他机制,以将经过纠正的数据从该表移至规范数据存储中
  • 使用查询侧过滤(例如,视图)以确保从数据库检索的数据始终处于正确状态,即使静态数据不是

避免所有这些问题的一种方法是,在发出写入的程序和实际数据库之间插入一个您控制的API层

这听起来像是您的问题的一部分,因为您甚至不知道所有生成不正确写入的地方-或其中太多的地方无法更新。这是一个令人恐惧的状态,但一开始不应该允许它出现。

一旦获得了不多的允许修改规范的生产数据存储中数据的系统,您就会遇到麻烦:无法集中维护有关该数据库的任何信息。更好的做法是,允许尽可能少的进程发出写入,并将其用作可以在插入之前对数据进行预处理的“关守”。确切的机制实际上取决于您的特定体系结构。


“如果您有错误地将数据插入数据库的程序,则这些程序将损坏,需要修复。” 从理论上讲也很不错,但现实是他们仍将添加记录,而一些委员会仍在继续争论是否使用“ NA”或“ None”。
JeffO'1

@JeffO:没有委员会应该辩论是否在数据库中存储 “ NA”,“ None”,NULL或其他内容。非技术性的利益相关者在什么样的数据而来的,是股权的数据库,它是如何使用的,而不是在内部表示。
丹尼尔·普赖登

@DanielPryden:在我的上一份工作中,我们有一个架构审查委员会(具有DBA小组委员会),负责审查跨域技术变更。技术性很强,但他们每两周开会一次,如果您没有为他们提供足够的详细信息,他们会推迟决定,直到您在以后的会议上才做。大多数不重要的系统更改通常不包括通过新代码添加功能,通常需要一个月左右的时间。
TMN

@DanielPryden-我参加了高层管理人员会议,讨论了文本框标签。您可能会争辩这与您将在应用程序或数据库中命名的名称无关,但是确实如此。
JeffO

在回应有关对此类更改获得额外批准的评论时:我对值“不正确”的观点以允许的值已经记录在某处为前提,这就是OP认为这些值应视为错误的原因。如果数据库的模式指定为允许值,则该值不是错误。关键是,如果您拥有的数据与架构不匹配,那么就会出现问题:您的优先级应该是使数据与架构匹配。根据团队的不同,可能涉及更改数据和/或架构。
丹尼尔·普里登

2

是否有任何好的架构或设计原则来处理稀有但可能为空的条目?

简单的答案-是的。

ETL

进行一些预先处理,以确保数据具有足够的质量以进入数据库。丢弃文件中的所有内容都应报告回去,任何干净的数据都可以加载到数据库中。

作为既是盗猎者(dev)又是游戏管理员(DBA)的人,我从痛苦的经历中了解到,除非被迫第三方不能解决他们的数据问题。不断地向后弯腰并遍历数据是一个危险的先例。

集市/存储库

在这种情况下,原始数据被推送到存储库DB中,然后将经过清理的版本推送到mart DB中,然后应用程序可以访问该数据库。

默认值

如果您可以将合理的默认值应用于列,则应该这样做,尽管如果这是现有数据库,则可能会涉及一些工作。

提前失败

试图在应用程序,报表套件,界面等的网关处简单地解决数据问题是很诱人的。我强烈建议您不要仅仅依靠它。如果将其他小部件挂接到数据库,则可能再次遇到相同的问题。解决数据质量问题。


+1这就是我要做的,收集所有数据并创建有效的数据集供您的应用程序处理。
Kwebble '16

1

只要您的用例允许使用良好的默认值安全地替换NULL,就可以SELECT使用ISNULL或在Sql语句中进行转换COALESCE。所以代替

 SELECT MyColumn FROM MyTable

一个可以写

 SELECT ISNULL(MyColumn,DefaultValueForMyColumn) FROM MyTable

当然,这仅在ORM允许直接操纵select语句或为生成提供可更改的模板时起作用。应该确保不会以这种方式掩盖任何“实际”错误,因此只有在用默认值替换为NULL的情况下才应用它才适用。

如果您能够更改数据库和架构,并且您的数据库系统支持此功能,则可以考虑将默认值子句添加到特定列,如@RobbieDee所建议。但是,这还需要修改数据库中的现有数据以删除任何以前插入的NULL值,并且此后将删除在正确和不完整的导入数据之间进行区分的功能。

根据我自己的经验,我知道使用ISNULL可以非常好地工作-过去,我不得不维护一个遗留应用程序,在该应用程序中,原始开发人员忘了向很多列添加NOT NULL约束,以后我们不容易添加这些约束。由于某些原因。但是在所有情况的99%中,数字列的默认值是0,文本列的默认值是空字符串是完全可以接受的。


尽管这样做有效,但最终您不得不为每个SELECT复制防御代码。更好的方法是在插入NULL时为列定义默认值,尽管出于各种原因这可能是不可能的。
罗比·迪

@RobbieDee:感谢您的发言,我相应地更改了答案。但是,这是否“好得多”尚有争议。当CRUD代码在一个地方时,重复的防御代码可能不是什么大问题。如果不是这样,则事先已经有一些代码重复。
布朗

简单的CRUD操作当然是理想的选择。但是在现实世界中,系统通常具有复杂的UI视图,用户生成的数据向导,报表等。但是,正如您所指出的那样,默认值需要从头开始,或者至少需要一些初始转换工作。您所描述的内容在棕地开发中可能更可取。
罗比·迪

最佳答案。新应用程序通常会添加一些新数据,这些数据可能无法控制。错误的NULL通常来自将旧数据导入重新设计的数据库中。为此,关闭了约束,以使其可以在几小时而不是几天内完成。当DBA尝试重新启用约束时,通常会出现“大失败”。由于从未计划过,因此管理人员通常会花很多时间来修复坏数据,因此仍然存在。所有应用程序都应通过插入默认值并报告或提示缺少数据来优雅地处理NULL。
DocSalvager

1

OP假定答案是将业务规则与数据库技术细节结合在一起。

从理论上讲,这应该不会发生,因此,如果这样做,则表明数据错误或代码中存在错误。错误具有不同的严重性,具体取决于哪个字段为空。也就是说,对于某些字段,应停止处理并通知某人;对于其他字段,应允许该处理继续进行,而仅通知某人。

这就是所有业务规则。业务规则并不关心null本身。就其所知,数据库可能具有空值9999,“ BOO!” ...这只是另一个价值。在RDBMS中,null具有有趣的属性,唯一的用途尚无意义。

唯一重要的是“空”对于给定的业务对象的含义...

是否有任何好的架构或设计原则来处理稀有但可能为空的条目?

是。

  • 将业务规则放在类中。
  • 音译应该在适当的代码层中,将业务类和数据存储区分开。如果您不能将其放入ORM代码中,至少不要将其放入数据库中。
  • 使数据库尽可能哑,此处没有业务规则。即使是无害的事情,例如默认值也会咬你。到过那里。
  • 验证去往和来自数据库的数据。当然,这是在业务对象的上下文中完成的。

在数据检索抛出异常是没有道理的。

问题是“我应该存储“不良”数据”吗?这取决于:

  • 可能会使用错误的数据 -切勿保存无效的对象或对象组合。遍布各地的复杂数据/业务关系。用户可以在任何给定的时间执行任何功能,可能在多个上下文中使用该业务实体。坏数据在保存时的影响(如果有的话)是未知的,因为它高度依赖于将来的使用。该数据没有统一/单个过程。
  • 如果有不良数据则无法继续 -允许保存不良数据。但是,在一切都有效之前,下一步不能继续进行。例如做个人所得税。从数据库中检索时,软件会指出错误,并且如果没有有效性检查,则无法将其提交给IRS。

0

处理空值的方法有很多,因此我们将从数据库层转到应用程序层。


数据库层

您可以禁止null ; 尽管这是不切实际的。

您可以按每列配置默认值

  • 它要求列是缺少insert,所以不包括显式空插入
  • 它可以防止insert错误地错过此列的行中的检测

您可以配置触发器,以便在插入时自动计算缺失值:

  • 它要求提供执行此计算所需的信息
  • 它会减慢 insert

查询层

您可以跳过出现不便的null

  • 它简化了主要逻辑
  • 它可以防止检测到“坏行”,因此需要另一个过程来检查它们
  • 它要求对每个查询进行检测

您可以在查询中提供默认值

  • 它简化了主要逻辑
  • 它可以防止检测到“坏行”,因此需要另一个过程来检查它们
  • 它要求对每个查询进行检测

注意:如果您有一些自动生成查询的方法,则对每个查询进行检测不一定是问题。


应用层

您可以预先检查该表是否被禁止null

  • 它简化了主要逻辑
  • 缩短了故障时间
  • 它需要保持预检查和应用程序逻辑的一致性

遇到禁止时,您可以中断处理null

  • 它避免了重复关于哪些列可以为哪些列null,哪些不能为列的知识
  • 它仍然相对简单(只需检查+返回/抛出)
  • 它要求您的过程是可恢复的(如果您已经发送了电子邮件,则不要发送两次或一百次!)

遇到禁止时,您可以跳过该行null

  • 它避免了重复关于哪些列可以为哪些列null,哪些不能为列的知识
  • 它仍然相对简单(只需检查+返回/抛出)
  • 它不需要您的过程可恢复

您可以一次或批量遇到禁止事件时发送通知null,这是上述其他方法的补充。然而,最重要的是“然后呢?”,最显着的是,如果您希望对行进行修补并且需要重新处理,则可能需要确保您有某种方法将已处理的行与需要处理的行区分开。正在重新处理。


鉴于您的情况,我将在应用程序中处理该情况,并结合以下两种方法:

  • 中断通知
  • 跳过通知

我倾向于在可能的情况下仅跳过,以确保取得一定的进展,特别是如果处理可能需要一些时间。

如果您不需要重新处理跳过的行,则只需记录它们就足够了,并且在处理结束时发送的电子邮件中包含跳过的行数将是一个适当的通知。

否则,我将使用边表将行固定(并重新处理)。该副表可以是简单的引用(没有外键),也可以是完整的副本:即使您花了很多时间,但后者又很昂贵,如果您没有时间null在清理主数据之前就去解决它,则它是必需的。


-1

在数据库类型到语言类型的转换或映射中可以处理空值。例如在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版本,那就太棒了……
Jon Raynor

我认为示例代码对于Java开发人员也是完全可以理解的。在我的情况下,我已经有一个ORM,因此不需要实施一个。但是您的答案仅解决了null的默认值,而在我的情况下,实际上更重要的情况是检测到null并触发操作(例如,将错误数据通知管理员)。
jhyot

啊,我将据此更新答案。
乔恩·雷诺

现在,您编辑的代码对任何null值都有一个默认操作(即,它是完全通用的)。这与我在原始问题中的第二个选项非常相似,即只是抛出null并将其捕获到某个地方。但是如上所述,我需要根据缺少的值来区分操作。
jhyot '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.