成员:使用唯一ID与域对象


10

在关于是否应将域对象或唯一ID作为方法/函数参数(此处将标识符与域对象用作方法参数)使用几个有用的答案之后,我有一个类似的问题:成员(先前的讨论没有设法解决)盖上这个)。使用唯一ID作为成员与使用对象作为成员的优缺点是什么。我要问的是强类型语言,例如Scala / C#/ Java。我应该有(1)

User( id: Int, CurrentlyReadingBooksId: List[Int])
Book( id: Int, LoanedToId: Int )

或(2),而不是(1)经历之后:是否应该为所有内容定义类型?

User( id: UserId, CurrentlyReadingBooksId: List[ BookId] )
Book( id: BookId, LoanedToId: UserId )

或(3)

User( id: Int, CurrentlyReadingBooks: List[Book]) 
Book( id: Int, LoanedTo: User)

虽然我没有想到拥有对象(3)的好处,但是拥有ID(2)和(1)的好处之一是,当我从数据库创建User对象时,不必创建Book对象,可能反过来取决于User对象本身,从而创建了一个无尽的链。对于RDBMS和No-SQL(如果它们不同)是否有通用的解决方案?

根据到目前为止的一些答案,改写我的问题:(使用ID应该是包装类型的ID)1)始终使用ID?2)总是使用对象?3)在序列化和反序列化存在递归风险时使用ID,否则使用对象吗?4)还有什么?

编辑:如果您回答应始终使用对象或在某些情况下使用对象,请确保回答其他回答者已发布的最大问题=>如何从数据库获取数据


1
感谢您提出的好问题,期待着您的关注。有点可惜的是您的用户名是“ user18151”,使用这种用户名的人会被一些人忽略:)
bjfletcher 2015年

@bjfletcher谢谢。我本人确实有那种na的感觉,但是我从来没有想过为什么!
0fnt 2015年

Answers:


7

域对象作为ID会产生一些复杂/细微的问题:

序列化/反序列化

如果将对象存储为键,则序列化对象图将变得极为复杂。stackoverflow由于递归,在对JSON或XML进行天真序列化时,您会得到错误。然后,您将必须编写一个自定义的序列化程序,该序列化程序将实际对象转换为使用其ID,而不是序列化对象实例并创建递归。

传递对象以确保类型安全,但仅存储ID,然后可以使用一个访问器方法,该方法在调用相关实体时会延迟加载相关实体。二级缓存将处理后续的调用。

细微的参考泄漏:

如果像在那里那样在构造函数中使用域对象,则将创建循环引用,这将非常困难,以允许回收未被有效使用的对象的内存。

理想情况:

不透明ID与int / long:

An id应该是完全不透明的标识符,不包含有关其标识的信息。但是它应该提供一些验证,证明它是其系统中的有效标识符。

原始类型打破了这一点:

intlong并且String是最常用的原料类型的RDBMS系统标识符。有实际原因的历史可以追溯到几十年,他们都妥协,要么配合到节能space或保存time或两者兼而有之。

顺序编号是最严重的违规者:

使用顺序ID时,默认情况下会将时间语义信息打包到ID中。这是不,直到它被使用。当人们开始编写对id的语义质量进行排序或过滤的业务逻辑时,他们为未来的维护人员带来了痛苦。

String 字段是有问题的,因为天真的设计师会将信息(通常也是时间语义)也打包到内容中。

这些也使得不可能创建分布式数据系统,因为12437379123不是全局唯一的。当您在系统中获得足够的数据时,几乎可以保证分布式系统中的另一个节点将创建具有相同编号的记录的机会。

然后黑客开始解决它,整个事情演变成一堆蒸腾的烂摊子。

当您开始尝试与其他系统共享数据时,忽略巨大的分布式系统(集群)也将成为一个噩梦。特别是当另一个系统不受您控制时。

您最终遇到了完全相同的问题,即如何使您的ID在全球范围内唯一。

创建和标准化UUID的原因如下:

UUID可能会遇到上面列出的所有问题,具体取决于Version您使用的方法。

Version 1使用MAC地址和时间来创建唯一ID。这很不好,因为它带有有关位置和时间的语义信息。这本身不是问题,而是天真的开发人员开始依赖该信息进行业务逻辑的时候。这还会泄漏可在任何入侵尝试中利用的信息。

Version 2使用用户UIDGIDdomian UIDGUI代替用户的时间Version 1就像Version 1数据泄漏一样糟糕,并且冒着将这些信息用于业务逻辑的风险。

Version 3相似,但用绝对具有语义含义MD5的某些内容的哈希表替换MAC地址和时间byte[]。没有数据泄漏的烦恼,byte[]无法从中恢复UUID。这为您确定性地创建UUID实例形式和某种外部提供了一种好方法。

Version 4 仅基于随机数是一个很好的解决方案,它绝对不包含语义信息,但不能确定地重新创建。

Version 5就像Version 4但使用sha1代替md5

域密钥和事务数据密钥

我对域对象ID的偏好是使用,Version 5或者由于某种技术原因而Version 3限制使用Version 5

Version 3 非常适合可能分散在许多计算机上的事务数据。

除非您受空间限制,否则请使用UUID:

它们被保证是唯一的,可以从一个数据库中转储数据并将其重新加载到另一个数据库中,而您不必担心实际引用不同域数据的重复ID。

Version 3,4,5 完全不透明,这就是应该的样子。

您可以使用a作为主键的一列UUID,然后可以使用复合唯一索引来代替原来的自然复合主键。

存储并没有必须CHAR(36)要么。您可以将它存储UUID在给定数据库的本机字节/位/数字字段中,只要它仍可索引即可。

遗产

如果您有原始类型并且不能更改它们,则仍然可以在代码中将它们抽象出来。

使用Version 3/5UUID,你可以通过在Class.getName()+ String.valueOf(int)作为byte[],有一个不透明的参考关键是重新创建和确定性。


如果我不清楚我的问题,我感到非常抱歉,我感到更糟(或实际上很好),因为这是一个很棒的,深思熟虑的答案,您显然花了很多时间。不幸的是,这不适合我的问题,也许它值得一个单独的问题?“为域对象创建id字段时应记住什么?”
0fnt 2015年

我添加了一个明确的解释。

现在明白了。感谢您花时间在答案上。
0fnt 2015年

1
顺便说一句,AFAIK世代垃圾收集器(我相信这是当今主要的GC系统)在GC的循环引用中应该不会有太大的困难。
0fnt 2015年

1
如果C-> A -> B -> AB被放置到Collectionthen中,A并且它的所有子元素仍然可以访问,则这些事情并不完全明显,并且可能导致细微的泄漏GC是最少的问题,图的序列化和反序列化是复杂性的噩梦。

2

是的,这两种方式都有好处,而且也有一个折衷办法。

List<int>

  • 节省记忆
  • 更快的类型初始化 User
  • 如果您的数据来自关系数据库(SQL),则不必访问两个表来获取用户,只需访问该Users

List<Book>

  • 用户访问图书的速度更快,该图书已预加载到内存中。如果您可以承受更长的启动时间以便更快地进行后续操作,则很好。
  • 如果您的数据来自诸如HBase或Cassandra之类的文档存储数据库,则已读书籍的值很可能会出现在用户记录中,因此您很容易在“在那里获得用户”时就获得了书籍。

如果您没有内存或CPU问题List<Book>,可以使用,使用User实例的代码将更加简洁。

妥协:

使用Linq2SQL时,为实体User生成的代码EntitySet<Book>在访问时会延迟加载。这样可以使您的代码保持干净,并且User实例较小(明智的使用内存)。


假设进行某种缓存,则预加载的好处将为空。我没有使用过Cassandra / HBase,所以不能谈论它们,但是Linq2SQL是一个非常特殊的情况(尽管我看不到即使在这种特定情况下,在一般情况下,延迟加载也不会阻止无限链接情况)
2015年

在Linq2SQL示例中,您实际上没有获得任何性能好处,只是获得了更简洁的代码。当从诸如Cassandra / HBase之类的文档存储中获取一对多实体时,绝大多数处理时间都花在查找记录上,因此,您不妨在该实体中获取所有许多实体(书籍,这个例子)。
ytoledano 2015年

你确定吗?即使我将Book和Users分别存储归一化了?在我看来,这应该只是网络延迟的额外费用。无论如何,一个人如何通用地处理RDBMS?(我编辑的问题清楚地提到了这一点)
0fnt

1

简短的经验法则:

ID在DTO中使用。
对象引用通常在域逻辑/业务逻辑和UI层对象中使用。

这是大型企业级项目中的通用架构。您将拥有可在这两种对象之间来回转换的映射器。


谢谢您的回答。不幸的是,虽然我确实了解Wiki链接带来的区别,但实际上我从未见过这一点(当然,我从未参与过大型的长期项目)。您是否有一个示例,其中出于两种不同目的以两种方式表示同一对象?
2015年

这里是关于映射一个实际问题:stackoverflow.com/questions/9770041/dto-to-entity-mapping-tool -有批评文章是这样的:rogeralsing.com/2013/12/01/...
herzmeister

真的很有帮助,谢谢。不幸的是,我仍然不了解如何使用循环引用加载数据?例如,如果某个用户引用一本书,而该图书引用了同一用户,那么您将如何创建该对象?
0fnt

查看存储库模式。您将有一个BookRepository和一个UserRepository。您将始终调用myRepository.GetById(...)或进行类似操作,存储库将创建对象并从数据存储中加载其值,或者从缓存中获取它。而且,子对象大多是延迟加载的,这也避免了在构造时必须处理直接循环引用。
herzmeister 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.