1

例子:

SalesOrder 由 SalesOrderHeader 和一个或多个 SalesOrderItems 组成。在编辑现有的 SalesOrder 时,可以修改 SalesOrderHeader 并且可以添加、修改和删除 SalesOrderItems。所有更改必须保存在单个事务中。多个用户可以以乐观并发同时编辑 SalesOrder。

我相信在单个事务中完成保存的要求鼓励我们在单个服务调用中同时传达 SaleOrderHeader 和 SalesOrderItems。将子数据与其父数据打包的含义是,需要对子数据是否被添加、修改或删除有所了解。

子实体的更改跟踪可以发生在服务器或客户端上。

服务器上的更改跟踪

这个策略的想法是,客户端可以根据自己的意愿修改 SalesOrder,而无需跟踪添加、修改或删除了哪些 SalesOrderItem。SalesOrderItems 的状态将在调用保存服务时在服务器上确定。

服务器应该在服务调用之间保持无状态。这意味着服务器无法保留任何有关 SalesOrder 在检索和最终保存之间状态的信息。如果服务器通过将修改后的对象图与数据库对象图进行比较来确定其实体的状态,则剩下的唯一选择。

使用 nHibernate,有一个合并功能来完成此操作。使用实体框架,最高投票的功能请求是添加此功能。还有一个用于 EF 的开源实现,称为GraphDiff

这在理论上听起来很棒,因为它使服务非常易于设计和使用。但是,我发现此策略存在两个主要问题。首先是性能。每次保存时必须将整个对象图发回。无论是否修改了 SalesOrderItem,都必须将其发回,否则服务器将假定它已被删除。第二个问题更为关键,它与并发有关。如果用户 1 将 SalesOrderItem 添加到 SalesOrder 并且用户 2 对相同的 SalesOrder 进行了更改,则当用户 2 保存时,服务器将假定用户 1 添加的 SalesOrderItem 应该被删除,因为它不包含在用户 2 的对象图中。我看不到在任何服务器端更改跟踪的实现中都可以防止这种情况发生。

客户端上的更改跟踪

另一种方法是让客户端跟踪其实体的更改并在调用保存服务时传达该状态。一个好处是客户端不需要发送其未更改的子实体。这有助于提高性能。缺点是所有实体都需要一个名为“ObjectState”的附加属性来跟踪它是否被添加、修改或删除。这使得服务器上的实体模型非常混乱,并且充满了与业务领域无关的关注点。这也让服务的不同消费者有责任维持这种状态。另一个问题是处理已删除的实体变得困难。SalesOrderHeader 是否应该维护已删除的 SalesOrderItems 列表?还是应该为 SalesOrderItems 分配一个必须由客户端 UI 过滤掉的已删除状态?

我知道微风javascript 库有自己的客户端实体跟踪实现,但我担心它的实现需要客户端和服务器端组件。服务层不应该隔离我们在任何一方使用的技术吗?如果非 JavaScript 客户想要使用我的服务怎么办?

问题

我认为这是一个常见的场景,应该由大多数服务实现来解决。我有没有做出任何不正确的假设,或者我做了什么不正常的事情或普通的事情?你实施了什么策略?有没有合理的替代方案?

4

2 回答 2

2

全面披露:我与Breeze合作,我认为对客户进行更改跟踪是可行的方法。客户端上的更改跟踪允许无状态服务器,减少客户端和服务器之间的流量,并允许离线使用。

在 Breeze 中,您提到的“ObjectState”称为EntityAspect,每个实体都有一个,但它不是域模型的一部分。服务器端实体不需要 EntityAspect,但服务器端服务必须知道如何处理来自客户端的实体状态信息。

基本上,服务需要根据来自客户端的信息来创建、更新或删除实体。现有的 Breeze 服务器端后端已经完成了所有这些工作(在.NET(EF 和 NHibernate)JavaPHPNodeRuby中),但您也可以自己编写。您的服务器只需要知道如何与客户端对话。

假设我们更新了 aSalesOrder并添加了一个新的SalesOrderItem. Breeze 客户端发送一个如下所示的保存包:

{
"entities": [
    {
        "Id": 123,
        "Title": "My Updated Title",
        "OrderDate": "2014-08-03T07:00:00.000Z",
        "entityAspect": {
            "entityTypeName": "SalesOrder:#My.DomainModel",
            "entityState": "Modified",
            "originalValuesMap": {
                "Title": "My Original Title"
            },
            "autoGeneratedKey": {
                "propertyName": "Id",
                "autoGeneratedKeyType": "Identity"
            }
        }
    },
    {
        "Id": -1,
        "SalesOrderId": 123,
        "ProductId": 456,
        "Quantity": 11,
        "entityAspect": {
            "entityTypeName": "SalesOrderItem:#My.DomainModel",
            "entityState": "Added",
            "originalValuesMap": {
            },
            "autoGeneratedKey": {
                "propertyName": "Id",
                "autoGeneratedKeyType": "Identity"
            }
        }
    }
]
}

此处,Id# 123 的 SalesOrder 已被修改(其 Title 已更改)。其中entityAspect包括originalValuesMap显示以前的标题是什么。
服务器将需要使用新值更新现有的 SalesOrder。服务器是否需要在应用更改之前从数据库中查询现有的 SalesOrder 取决于实现。

添加了一个新的 SalesOrderItem。在客户端上为其创建了一个临时 ID -1。服务器需要创建并持久化一个新的 SalesOrderItem 并为其生成一个真实的 Id。

来自服务器的响应应包含已创建和更新的实体,以及显示服务器生成的密钥映射到临时客户端密钥的 KeyMapping 信息,以便客户端可以替换它们。

更改跟踪不是一个简单的问题,但 Breeze 会尝试为您完成最困难的部分。

于 2014-08-21T21:35:13.607 回答
1

我想借鉴史蒂夫的回答。

我们应该清楚:在关系数据模型中实现订单图(又名“订单聚合”)事务的责任落在了开发人员身上。BreezeJS(和 .NET 服务器的 Breeze 助手)可以提供便利,但您必须使其工作。

完成这项工作的关键是将聚合的根元素 - 订单 - 包含在聚合内任何实体的所有更改中。如果您添加、删除或修改 OrderItem,请确保同时修改 Order

如何?通过增加 Order 的并发属性(例如rowVersion)并确保 Breeze 知道这是您的并发属性。

如果要确保 Order 聚合的一致性,则必须实现根实体乐观并发。

现在,您可以检测其他人是否对 Order 聚合的任何部分进行了更改。这可能是对 Order 的更改或对其 OrderItem 之一的添加/修改/删除。

当您保存更改的订单聚合时,您不必在更改集中包含所有 OrderItem。您只需要包含添加/修改/删除的 OrderItems。

当然,其他一些用户可能会在您保存之前对 Order 聚合进行更改。当您尝试保存时,保存将失败并出现乐观并发错误。

在检测到 Order 的乐观并发错误时,确保客户端从缓存中删除整个订单聚合 - Order 及其所有 OrderItems - 然后重新获取聚合不要只是重新获取根 Order 实体并开始弄乱它的物品。确保从缓存中删除整个聚合,然后重新获取它(订单及其项目)。

如果每个人都遵循此协议,您将在服务器上处于良好状态。

于 2014-08-23T06:35:42.067 回答