【问题标题】:Is it a good idea to create a custom type for the primary key of each data table?为每个数据表的主键创建自定义类型是个好主意吗?
【发布时间】:2010-01-11 17:05:51
【问题描述】:

我们有很多关于“Ids”数据行的代码;这些主要是整数或指导。我可以通过为每个数据库表的 id 创建一个 不同的结构 来使这段代码更安全。 然后类型检查器将帮助查找传递错误 ID 的情况。

例如,Person 表有一个名为 PersonId 的列,我们的代码如下:

DeletePerson(int personId)
DeleteCar(int carId)

如果有会更好:

struct PersonId
{
   private int id;
   // GetHashCode etc....
}

DeletePerson(PersionId persionId)
DeleteCar(CarId carId)
  • 有没有人有过真实的生活经历 东这个?

  • 开销是否值得?

  • 或者更多的痛苦值得吗?

(这也可以更容易地改变数据库中主键的数据类型,这是我最初想到的理想方式)


请不要说使用 ORM 对系统设计进行其他重大更改,因为我知道 ORM 会是更好的选择,但目前这不在我的能力范围内。不过,我可以对我目前正在开发的模块进行类似上述的小改动。

更新: 请注意,这不是一个 Web 应用程序,并且 Id 保存在内存中并通过 WCF 传递,因此在边缘没有与字符串之间的转换。 WCF 接口没有理由不能使用 PersonId 类型等。 PersonsId 类型等甚至可以在 WPF/Winforms UI 代码中使用。

系统唯一固有的“无类型”位是数据库。


这似乎取决于花费时间编写编译器可以更好地检查的代码或花费时间编写更多单元测试的成本/收益。我更愿意花时间进行测试,因为我希望在代码库中至少看到一些单元测试。

【问题讨论】:

  • 类型检查器将如何帮助您检查是否传递了无效 ID?如果您的代码试图传递一个预期为 int 的 GUID,它无论如何都应该爆炸。我不确定您想要了解的内容是否会为您提供更多保护,但它会不必要地使您的代码混乱。
  • @Chris,对不起,我试图抓住一个 CarID 被传递到 PersonID 的情况,当它们都是整数时。

标签: c# .net database design-patterns


【解决方案1】:

很难看出它是多么值得:我建议仅作为最后的手段,并且只有当人们在开发过程中实际上混合标识符或报告难以保持正确时才这样做。

尤其是在 Web 应用程序中,它甚至无法提供您所希望的安全性:通常您无论如何都会将字符串转换为整数。在很多情况下,您会发现自己编写了这样的愚蠢代码:

int personId;
if (Int32.TryParse(Request["personId"], out personId)) { 
    this.person = this.PersonRepository.Get(new PersonId(personId));
}

处理内存中的复杂状态肯定会改善强类型 ID 的情况,但我认为Arthur's idea 更好:为避免混淆,要求使用实体实例而不是标识符。在某些情况下,性能和内存方面的考虑可能会使这变得不切实际,但即使这样也应该很少见,以至于代码审查在没有负面副作用的情况下同样有效(恰恰相反!)。

我已经开发了一个系统来实现这一点,但它并没有真正提供任何价值。我们没有像您所描述的那样模棱两可,而且在面向未来的方面,它使得在没有任何回报的情况下实施新功能变得更加困难。 (无论如何,两年内没有任何 ID 的数据类型发生变化 - 这肯定会在某个时候发生,但据我所知,目前的投资回报率是负数。)

【讨论】:

  • 这不是一个网络应用程序,并且在内存中保存了很多复杂的状态,包括 ID。但是我在网络应用程序中看到了很多上述内容,因此 +1
  • 您提供的 Web 场景所需的显式转换并不是真正的缺点,它会迫使您在其输入边界记录/考虑参数的类型。
  • @Frank - 在我描述的情况下,您已经拥有 1) 请求键和 2) 局部变量名称,两者都描述了输入边界处的参数内容。我只是看不到强类型标识符类在那种情况下增加了什么价值。
【解决方案2】:

我不会为此创建一个特殊的 ID。这主要是一个测试问题。您可以测试代码并确保它完成了它应该做的事情。

您可以在系统中创建一种标准的做事方式,而不是通过传入要操作的整个对象来帮助将来的维护(类似于您提到的)。当然,如果您将参数命名为 (int personID) 并且有文档,那么任何非恶意程序员在调用该方法时都应该能够有效地使用代码。传递整个对象将执行您正在寻找的类型匹配,这应该是一种标准化的方式。

我只是看到有一个特殊的结构来防止这种情况,因为增加了更多的工作却没有什么好处。即使您这样做了,也可能有人会找到一种方便的方法来创建“辅助”方法并绕过您放置的任何结构,因此这实际上并不能保证。

【讨论】:

    【解决方案3】:

    您可以选择 GUID,就像您自己建议的那样。然后,您不必担心将“42”的人员 ID 传递给 DeleteCar() 并意外删除 ID 为 42 的汽车。GUID 是唯一的;如果由于编程错误而在代码中将人员 GUID 传递给 DeleteCar,则该 GUID 将不是数据库中任何汽车的 PK。

    【讨论】:

    • 在我看来是所有可用答案中的最佳答案。
    • 这是解决这个特定问题的好方法,并且可能是合适的,但决定是否对特定主键使用 GUID 或整数取决于许多其他因素。对“GUID Int Primary Key”的快速 SO 搜索提供了几个很好的简要介绍。
    【解决方案4】:

    您可以创建一个简单的Id 类,它可以帮助区分两者的代码:

    public class Id<T>
    {
        private int RawValue
        {
            get;
            set;
        }
    
        public Id(int value)
        {
            this.RawValue = value;
        }
    
        public static explicit operator int (Id<T> id) { return id.RawValue; }
    
        // this cast is optional and can be excluded for further strictness
        public static implicit operator Id<T> (int value) { return new Id(value); }
    }
    

    这样使用:

    class SomeClass
    {
         public Id<Person> PersonId { get; set; }
         public Id<Car> CarId { get; set; }
    }
    

    假设您的值只能从数据库中检索,除非您将值显式转换为整数,否则无法在彼此的位置使用两者。

    【讨论】:

    • 我追求用 Linq-to-SQL 做这样的事情,不幸的是它不允许我使用我的自定义类型和 ID 列。因此,您可能会遇到其他 ORM 层的问题。最后我没有这样做,但我认为这是一个合理的事情。不过这将是不寻常的。
    • 我认为你混淆了隐式/显式。 Id 类型应该可以隐式转换为 int,而 int 只能显式转换为 Id。
    • 我更喜欢这种方式,因为远离类型的转换很容易看到。我希望最终用户永远不会将数据库 ID 视为 int。各有各的。我们有一个 ORM 系统,它使用这些 Id 类(嗯,类似的东西),效果很好。
    【解决方案5】:

    在这种情况下,我认为自定义检查没有多大价值。您可能希望增强您的测试套件以检查是否发生了两件事:

    1. 您的数据访问代码始终按预期工作(即,您不会将不一致的密钥信息加载到您的类中并因此而被滥用)。
    2. 您的“往返”代码按预期工作(即,加载记录、进行更改并将其保存回来不会以某种方式破坏您的业务逻辑对象)。

    拥有一个您可以信任的数据访问(和业务逻辑)层对于解决您在尝试实现实际业务需求时会遇到的更大的问题至关重要。如果您的数据层不可靠,您将花费大量精力来跟踪(或者更糟的是,解决)当您将负载加载到子系统时出现在该级别上的问题。

    如果您的数据访问代码在面对不正确的使用(您的测试套件应该向您证明的内容)时是健壮的,那么您可以在更高级别上放松一点,并相信它们会抛出异常(或者无论您正在处理什么)与它)被滥用时。

    您听到人们建议使用 ORM 的原因是这些工具以可靠的方式处理了许多此类问题。如果您的实现足够远以至于这样的切换会很痛苦,请记住,如果您真的希望能够信任(因此忘记一定程度上)您的数据访问权限。

    代替自定义验证,您的测试套件可以注入代码(通过依赖项注入),在测试运行时对您的密钥进行稳健的测试(访问数据库以验证每个更改),并注入省略或限制此类测试的生产代码出于性能原因。您的数据层将在失败的键上抛出错误(如果您在那里正确设置了外键),因此您也应该能够处理这些异常。

    【讨论】:

    • 如果只是...,当前的大部分代码只是执行一个catch,然后记录异常,然后将一些默认结果返回到下一层。 (毕竟让客户认为这个系统可以正常工作,然后才能让系统正常工作更重要。)
    • 哎哟。从好的方面来说,您有日志记录,可用于构建此类测试,以防止此类错误将来再次发生(我猜现在此类测试不常见?)如果您测试数据库代码可以很简单能够在每次测试运行之前将您的测试数据库设置为已知状态。我们使用“穷人的”解决方案来捕获我们的起始状态,然后在回滚的事务中运行 all 测试代码(成功失败)。这样,每个测试都可以使用原始数据库(更长的测试有一个外部 TRANSACTION)。
    【解决方案6】:

    我的直觉说这不值得麻烦。我对您的第一个问题是您是否真的发现了传递错误 int 的错误(在您的示例中是 Car ID 而不是 Person ID)。如果是这样,则可能更多的是整体架构更差的情况,因为您的域对象具有过多的耦合,并且在方法参数中传递了过多的参数,而不是作用于内部变量。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多