在业务逻辑中引用数据库值


43

我想这是关于硬编码和最佳实践的另一个问题。假设我有一个值列表,可以说是水果,存储在数据库中(它必须在数据库中,因为该表用于其他目的,例如SSRS报告),其ID为:

1 Apple 
2 Banana 
3 Grapes

我可以将它们呈现给用户,他选择一个,将其作为FavouriteFruit存储在他的个人资料中,并将ID存储在他的数据库记录中。

当涉及到业务规则/域逻辑时,将逻辑分配给特定值的建议是什么。假设用户选择了葡萄我想执行一些额外的任务,那么引用葡萄值的最佳方法是什么:

// Hard coded name
if (user.FavouriteFruit.Name == "Grapes")

// Hard coded ID
if (user.FavoriteFruit.ID == 3) // Grapes

// Duplicate the list of fruits in an enum
if (user.FavouriteFruit.ID == (int)Fruits.Grapes)

或者是其他东西?

当然,由于FavouriteFruit将在整个应用程序中使用,因此可以将列表添加或编辑。

有人可能会决定将“葡萄”重命名为“葡萄”,这当然会破坏硬编码的字符串选项。

硬编码的ID尚不完全清楚,如图所示,您可以添加注释以快速识别它是哪个项目。

枚举选项涉及从数据库中复制数据,这似乎是错误的,因为它可能不同步。

无论如何,在此先感谢您的任何意见或建议。


1
谢谢大家:这些建议和一般建议确实很有帮助。@RemcoGerlich的想法是,将用于显示目的的字符串的关注点分开,并将其作为更可读的代码的查找代码分开,这非常好。
凯特

1
我要给@Mike Nakis您预装的对象一个想法,尽管这似乎是两全其美。
凯特

1
我建议对您的第一个解决方案进行更改。让您的表包含第3列,说明如何处理该表,然后使用该字段确定要执行的代码。不是显示字段,可以在多个水果之间共享。
Kickstart 2015年

1
枚举选项涉及从数据库中复制数据,这似乎是错误的,因为它可能不同步。我真的很喜欢 这就像两次进入簿记。如果分类帐的两端都不平衡,您就会知道出了点问题。它使更改事情变得更加刻意。
Radarbob 2015年

1
嗯...如果ID与字符串之间存在1:1的关系,则这是多余的,并且两者都没有意义。字符串可以用作数据库密钥,也可以用作整数。 MyApplication.Grape.ID可以说是口吃。“ Apple”不是ID为3的“ Red_Apple”,也不是4。因此,将“ Apple”重命名为“ Red_Apple”的可能性比声明3为4(甚至3)更有意义。枚举的关键是抽象出其数字DNA。因此,也许是时候真正解耦在业务模型中实际上没有任何意义的任意关系数据库密钥了。
Radarbob 2015年

Answers:


31

不惜一切代价避免使用字符串和魔术常数。 它们完全是不可能的,甚至不应该将它们视为选项。这似乎给您留下了一个可行的选择:标识符,即枚举。但是,还有另外一种选择,我认为这是最好的。我们将此选项称为“预加载的对象”。使用预加载的对象,您可以执行以下操作:

if( user.FavouriteFruit.ID == MyApplication.Grape.ID )

刚才发生的事情是,我显然已将整行Grape内存加载到内存中,因此可以随时将其ID用于比较。如果您碰巧正在使用对象关系映射(ORM),它看起来会更好:

if( user.FavouriteFruit == MyApplication.Grape )

(这就是为什么我称其为“预加载的对象”。)

因此,我要做的是,在启动期间,我将所有“枚举”表(如一周中的几天,一年中的月份,性别等小表)加载到主应用程序域类中。我按名称加载它们,因为显然MyApplication.Grape必须接收名为“ Grape”的行,并且我断言每个元素都被找到了。如果不是这样,我们将在启动过程中确保出现运行时错误,这是所有运行时错误中危害最小的。


17
我不同意答案,但我认为“不惜一切代价避免使用字符串和魔术常数”的命令与其余的答案不同,这实际上要求至少有一个使用魔术常数或字符串的地方填充“预加载的对象”。我认为,这是值得注意的,因为有一些方法可以完全避免“字符串和魔术常数”,尽管它通常比其价值更令人迷惑……
svidgen

2
@svidgen会不会同意,在各处散布按名称的绑定与仅一次散布按名称的绑定,加载具有相同名称的记录的内容,并且仅这样做之间存在根本区别在启动时,运行时错误与编译错误几乎一样良性?无论如何,即使您提到过混淆,即使通过名称避免即使是最细微的绑定也总是很有趣,所以我很想知道您的想法。
Mike Nakis 2015年

哦,我完全同意。而且,考虑到OP的性质,我只建议将此答案从“不惜一切代价”更改为“在任何可能和可行的情况下”或类似的方法中受益。...如果我有更多的时间,仅出于完整性考虑,我会写一个答案来处理某种元编程废话...但是,这不是OP(或大多数情况下的任何人)可能需要的。但是,元编程解决方案将更符合您的第一个陈述。
svidgen 2015年

1
@ user469104的区别在于ID可能会更改,并且应用程序仍将正确加载所有行并正确执行所有比较。此外,你可以自由地以任何方式你喜欢重构代码和重命名行,在那里你必须去寻找的东西,以修正的唯一地方是在应用程序的启动,这往往是非常明显的:Grape = fetchRow( Fruit.class, NameColumn, "Grape" ); 如果你错误地做某事,AssertionError就会让您知道。
Mike Nakis 2015年

1
@grahamparks只不过enum是一个魔术字符串。重点是将所有按名称绑定集中在一个地方在启动过程中对其全部进行验证,然后键入安全性
Mike Nakis 2015年

7

检查字符串是最易读的方法,但它有双重作用:既用作标识符又用作描述(由于不相关的原因可能会更改)。

我通常将这两个职责划分为不同的字段:

id  code    description
 1  grape   Grapes
 2  apple   Apple

描述可能会更改(但“ Grapes”不会更改为“ Banana”),但是代码永远都不会更改。

尽管这主要是因为我们的ID几乎总是自动生成的,所以不太适合。如果您可以自由选择ID,也许可以保证它们始终正确并使用它们。

另外,有人真正将“葡萄”编辑成“葡萄”的频率是多少?也许这都不是必需的。


8
我不认为更多的冗余解决方案...
Robbie Dee 2015年

4
我也考虑过这个选项,并且已经尝试过,但是最终还是这样:在某些时候,“苹果”必须区分为“ green_apple”和“ red_apple”。但是因为在代码中的许多地方已经使用了“苹果”,所以我无法重命名它,所以我必须有“苹果”和“ green_apple”。结果,我里面的谢尔顿阻止我睡了好几个晚上,直到我进去并将所有东西都重构为“预装的物体”。(请参阅我的回答。)
Mike Nakis 2015年

1
我当然喜欢您的预加载对象,但是如果您的“苹果”与众不同,那么您是否无论选择哪种方法都不必遍历所有内容?
RemcoGerlich 2015年

您甚至可以使用一个单独的描述名称表来支持国际化。
埃里克·艾德

1
@MikeNakis and Refactoring本质上是对整个代码库的搜索与替换,将Fruit.Apple替换为Fruit.GreenApple。如果我使用Hardcoded String值,我将在整个代码库中执行“搜索并替换”,将“ apple”替换为“ green_apple”,这是差不多的事情。-重构确实感觉更好,因为IDE正在执行“替换”。
Falco 2015年

4

您在这里期望的是编程逻辑可以自动适应不断变化的数据。像Enum这样的简单静态选项在这里不起作用,因为您无法在运行时中添加额外的枚举。

我看到的一些模式:

  • 枚举+默认值可以防止出现破坏您程序一天的全新数据库条目。
  • 在数据库本身中编码要执行的操作(业务逻辑)。在许多情况下,这很有可能,因为许多逻辑被重用。逻辑的实现应在程序中。
  • 数据库中的其他属性/列在程序正确部署之前将新值标记为程序中的“将被忽略”。
  • 代码路径周围的快速失败机制会从数据库加载/重新加载值。(如果相应的动作不在程序中并且未标记为忽略,则不要刷新)。

总的说来,我喜欢数据在涉及其所暗示的动作方面是完整的-即使动作本身可以在其他地方实现。任何确定独立于数据的操作的代码都已将您的数据表示分片,这很可能会分散并导致错误。


4

将它们存储在两个地方(在表和ENUM中)并不坏。原因如下:

通过将它们存储在数据库表中,我们可以通过外键在数据库中强制执行引用完整性。因此,当您将一个人或任何实体与一个水果相关联时,它仅是数据库表中存在的一个水果。

将它们存储为ENUM也很有意义,因为我们可以编写没有魔术字符串的代码,并使代码更具可读性。是的,它们确实必须保持同步,但是将行添加到ENUM和将新的insert语句添加到数据库的难度实际上是多么困难。

一件事,一旦定义了ENUM,就不要更改它的值。例如,如果您有:

  • 苹果
  • 葡萄

请勿将葡萄重命名为葡萄。只需添加一个新的ENUM。

  • 苹果
  • 葡萄
  • 葡萄

如果您需要迁移数据,请应用更新以将所有Grape迁移到Grapes。


作为下一步,我在商店中工作,这些商店的元数据值在表中具有删除标志,以指示不应使用它们(已弃用或存在较新版本)。
罗比·迪

1

您问这个问题是对的,实际上,这是一个不错的问题,因为您正试图防止对不准确条件的评估。

就是说,评估(您的if条件)不一定是您如何进行评估的重点。相反,请注意传播将导致“不同步”问题的更改的方式。

弦法

如果必须使用字符串,为什么不通过UI公开更改列表的功能?在变化的设计系统,以便GrapeGrapes,例如,您更新所有的记录,目前引用Grape

ID方法

尽管某些可读性有所妥协,但我始终希望引用ID。The list may be added to如果您公开了此类UI功能,您可能还会再次收到通知。如果您担心更改ID的项目的重新排序,请将该更改再次传播到所有相关记录。与上面类似。另一个选择(遵循正确的规范化约定,将有一个enum / id列-并引用一个更详细的FruitDetail表,该表具有一个可以查询的“ Order”列)。

无论哪种方式,您都可以看到我建议控制列表的修改或更新。是否通过使用ORM或其他一些数据访问来执行此操作,取决于您的技术细节。本质上,您正在做的是要求人们远离数据库进行此类更改-我认为这很好。大多数主要的CRM都会提出相同的要求。


1
在数据库中,将数字ID存储为子记录,专门用于避免该问题。这个问题是关于如何与编程语言接口的。
Clockwork-Muse

1
@ Clockwork-Muse-避免哪个问题?那没有道理。
JᴀʏMᴇᴇ

我使用了很多ID方法,但是ID被锁定并且无法更改。当然,附加的字符串可能是因为人们经常喜欢将事物重命名为“卡车”而变成“卡车”等,而事物本身(由ID表示)不会改变。
Brian Knoblauch 2015年

如果您采用ID方法,那么如何处理开发数据库和生产数据库?使用自动递增的ID时,以不同顺序将项目添加到两个DB将产生不同的ID。
保护者

不必一定是自动递增的吗?在这种情况下不应该这样,特别是如果它是我们正在使用的基础枚举的整数值。
JᴀʏMᴇᴇ

0

一个非常普遍的问题。尽管复制数据客户端似乎违反了DRY原则,但这实际上是由于各层之间范式的差异。

枚举(或其他)与数据库不一致实际上也不是那么普遍。您可能已经向元数据表中推出了另一个值,以支持客户端代码中尚未使用的新报告功能。

有时它也会以其他方式发生。新的枚举值被添加到客户端,但是只有在DBA可以应用更改之前,数据库更新才能发生。


是的,您已经描述了问题。您有什么解决方案?
保护者

1
@Protectorone你假设有一个一劳永逸的解决方案这是我的经验,一个错误的假设。最好的希望是,某些业务实体拥有问题域,因此至少您可以看到哪一方滞后-客户端方还是数据库方。银行和金融在这方面通常非常高效,而零售业的效率则明显下降...
罗比·迪

0

假设我们在谈论什么本质上是静态查找,那么第三个选项-枚举-基本上是唯一明智的选择。如果不涉及数据库,您将要做的事情很有意义。

然后,问题就变成了关于如何使数据库中的枚举和静态/查找表保持同步的问题,不幸的是,这不是我要完全解决的问题。

通过选择,我将用代码进行所有模式维护,因此,我可以保持应用程序的构建与预期模式版本之间的关系,因此可以很容易地使查找和枚举保持同步,但必须记住这一点。做。最好是自动化程度更高(还要进行自动化集成测试以确保枚举和查找匹配将有所帮助),但这不是我所实现的。


1
我不认为这些只是静态查找,否则它们只能从数据库中提取并按原样使用。据我了解,问题在于何时根据所使用的查找值来应用业务逻辑。但是,除此之外,是的-枚举通常用于此目的。
罗比·迪

好的,我需要一个更好的术语“用于静态查找”,您所描述的上下文就是我的意思:)关键是“静态”-这些值不会改变,问题是添加新值并更改“标签”(但不是目的)。
Murph 2015年
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.