问题 为每个数据表的主键创建自定义类型是一个好主意吗?


我们有很多代码通过“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应用程序,并且Ids保存在内存中并通过WCF传递,因此没有转换到/来自边缘的字符串。没有理由WCF接口不能使用PersonId类型等.PersonsId类型等甚至可以在WPF / Winforms UI代码中使用。

唯一固有的 “类型化” 系统的一部分是数据库。


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


7182
2018-01-11 17:05


起源

类型检查器如何帮助您检查是否传递了无效的ID?如果你的代码试图传递一个预期有int的GUID,它应该会炸弹。我不确定你想要的是什么会给你更多的保护,但它会不必要地混乱你的代码。 - Chris
@Chris,对不起我试图抓住CarID被传递给PersonID的instread的情况,当它们都是整数时。 - Ian Ringrose


答案:


我不会为此特别提出异议。这主要是一个测试问题。您可以测试代码并确保它完成预期的操作。

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

我只是看到有一个特殊的结构来防止这种情况,因为增加了更多的工作而没什么好处。即使你这样做了,也有人可以找到一种方便的方法来制定一个“帮助”方法并绕过你所设置的任何结构,所以它实际上不是一个保证。


2
2018-01-11 17:17





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

特别是在Web应用程序中,它甚至不会提供您希望的安全性:通常您无论如何都会将字符串转换为整数。有太多的情况你会发现自己编写如下的愚蠢代码:

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

处理内存中的复杂状态肯定会改善强类型ID的情况,但我认为 亚瑟的想法 甚至更好:为了避免混淆,需要实体实例而不是标识符。在某些情况下,性能和内存考虑可能会使这种做法变得不切实际,但即使是那些也应该是非常罕见的,以至于代码审查在没有负面副作用的情况下同样有效(完全相反!)。

我曾经在一个这样做的系统上工作,并没有真正提供任何价值。我们没有像您所描述的那样含糊不清,并且在未来验证方面,它使得在没有任何回报的情况下实现新功能稍微困难一些。 (没有ID的数据类型在两年内发生了变化,无论如何 - 它肯定会在某个时刻发生,但据我所知,目前投资回报率为负值。)


4
2018-01-11 17:26



这不是一个Web应用程序,并且在内存中保留了很多复杂的状态,包括ID。但是我在网络应用程序中看到了很多,因此+1 - Ian Ringrose
您提供的Web场景所需的显式强制转换并不是真正的缺点,它会强制您记录/考虑其输入边界处的参数类型。 - Frank Schwieterman
@Frank - 在我描述的情况下你已经有1)一个请求键和2)一个局部变量名,两者都描述了输入边界处参数的内容。我只是没有看到强类型标识符类在该场景中添加了什么值。 - Jeff Sternal


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


2
2018-01-11 17:32



对我来说,看起来是最好的答案。 - Ignacio Soler Garcia
这是解决这个特定问题的一个很好的解决方案,并且可能是合适的,但是决定是否对特定主键使用GUID或整数取决于许多其他因素。对“GUID Int Primary Key”的快速SO搜索提供了几个很好的简要介绍。 - Jeff Sternal


你可以创建一个简单的 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; }
}

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


2
2018-01-11 18:57



我想用Linq-to-SQL做这样的事情,遗憾的是它不允许我使用我的自定义类型和ID列。因此,您可能遇到其他ORM图层的问题。最后我没有这样做,但我认为这是合理的。这不寻常。 - Frank Schwieterman
我认为你有隐含/明确的混淆。 Id <>类型应该可以隐式转换为int,而int应该只能显式转换为Id <>。 - Frank Schwieterman
我更喜欢这种方式,因为远离类型的演员阵容很容易看到。我更喜欢最终用户永远不会将数据库ID视为int。给每个人自己。我们有一个ORM系统,它使用这些Id类(好吧,类似的东西),效果很好。 - user7116


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

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

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

相反,如果您的数据访问代码在使用不当(您的测试套件应该向您证明)时是健壮的,那么您可以在更高级别放松一点并相信它们会抛出异常(或者您正在处理它)被虐待时

您听到人们建议使用ORM的原因是这些工具中的许多问题都是以可靠的方式处理的。如果您的实现远远不够,那么这样的转换会很痛苦,请记住,如果您真的希望能够信任,那么您的低级数据访问层需要像良好的ORM一样健壮(因此忘记了在一定程度上)您的数据访问。

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


1
2018-01-11 17:29



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


我的直觉说这不值得麻烦。我的第一个问题是你是否确实发现了错误的int被传递的错误(在你的例子中是Car ID而不是Person ID)。如果是这样,可能更糟糕的是整体架构更糟糕,因为你的Domain对象有太多的耦合,并且在方法参数中传递了太多的参数而不是对内部变量起作用。


1
2018-01-11 17:30