2

据我了解,基于 OData 的服务支持“upsert”(即,插入一行,或者如果具有此键的行已经存在则更新它)的常用方法是通过包含行过滤器的 PUT 请求和分区键。

http://myaccount.table.core.windows.net/mytable(PartitionKey='myPartitionKey', RowKey='myRowKey1')

据我所知,这就是 Azure 表存储支持 upsert 的方式。但据我所知,如果您在使用 .NET Framework 的内置实现的 OData 服务上尝试相同的操作DataService<T>,则只有在该行已经存在时才会成功。如果该行不存在,我会收到 404 错误。

换句话说,这仅适用于更新,不适用于插入。

我怀疑 upsert 根本不受支持,但无法找到明确的答案。谁能告诉我该怎么做,或者确认我绝对不能?

4

2 回答 2

2

您可以尝试创建自己的自定义查询提供程序(IDataServiceQueryProvider 的实现)。如果用户正在请求一个不存在的对象并且当前 http 请求的方法是 PUT,则返回一个具有给定 id 的新对象。我认为内置的更新提供者应该能够从那里处理它并更新记录。否则,您可能还需要自己的更新提供程序。

使用WCF 数据服务工具包可能会使您的工作更轻松。否则,您将不得不编写自己的 linq 提供程序,这对于集成测试来说听起来有点矫枉过正。

是 msdn 上很好的系列博客文章,描述了如何构建自定义 ds 提供程序。

于 2013-02-28T10:27:30.623 回答
2

好的,我已经尝试了 Jason Freitas 的建议,结果太复杂而无法在评论中解决,所以我添加了一个答案。

tl; dr:您可以做到这一点,尽管解决方案围绕IDataServiceUpdateProviderand展开IUpdatable。(Jason 建议IDataServiceQueryProvider,这似乎没有帮助。)但是,问题在于DataService<T>它并不是真正设计为支持 upsert,也不是用于更新的接口,所以虽然你可以让它工作,但解决方案是hack(而且不是很好),我怀疑将来可能会导致问题。

长版:

我已经在实现IUpdatable,这是支持更新、插入和删除所需的。IDataServiceQueryProvider不添加任何与更新支持相关的内容,因此IUpdatable是关键。我最初错误地认为IUpdatable.GetResource即使请求的项目不存在也安排返回项目会弄乱查询行为。但是当然,针对的查询DataService<T>不会通过IUpdatable,因此无论请求什么,都可以让该方法返回一个对象。

这样做非常复杂,而且事实证明这还不够。这是代码:

public object GetResource(IQueryable query, string fullTypeName)
{
    var item = query.Cast<object>().SingleOrDefault();
    if (item == null && fullTypeName != null)
    {
        var ctor = Type.GetType(fullTypeName).GetConstructor(Type.EmptyTypes);
        if (ctor != null)
        {
            item = ctor.Invoke(null);
            PopulatePutStandin(query.Expression, item);
        }
    }

    return item;
}

private void PopulatePutStandin(Expression expression, object item)
{
    var call = expression as MethodCallExpression;
    if (call != null && call.Method.Name == "Where" && call.Method.DeclaringType == typeof(Queryable))
    {
        foreach (Expression arg in call.Arguments)
        {
            var ux = arg as UnaryExpression;
            if (ux != null)
            {
                var op = ux.Operand as LambdaExpression;
                if (op != null)
                {
                    var bx = op.Body as BinaryExpression;
                    if (bx != null && bx.Method.Name == "op_Equality")
                    {
                        var left = bx.Left as MemberExpression;
                        var right = bx.Right as ConstantExpression;
                        if (left != null && right != null)
                        {
                            var prop = left.Member as PropertyInfo;
                            if (prop != null)
                            {
                                prop.SetValue(item, right.Value);
                            }
                        }
                    }
                }
            }
            else
            {
                PopulatePutStandin(arg, item);
            }
        }
    }
}

GetResource方法是IUpdatable接口的一部分,DataService<T>当它接收到 PUT 时,它是用来尝试查找现有资源的方法。如您所见,如果对象还不存在,它只是构造一个新实例。然而,仅仅让新对象保持默认状态是不够的。PartitionKeyRowKey必须匹配传入 PUT 请求的值。而且您不会直接被告知这些值 - 它们嵌入在查询中。

因此,我编写了该PopulatePutStandin方法以将这些值从查询中提取出来。它遍历表示查询的调用表达式链,查找Where调用。(这不处理任何其他 LINQ 运算符,但对于更新/插入,您不应该看到任何更复杂的东西。)对于每个Where测试特定属性是否具有特定值的子句,我的代码将该属性设置为新对象的值。在实践中,这将最终只设置PartitionKeyandRowKey因为这些是唯一Where存在于 upsert 的子句,但是编写不查找任何特定属性的代码更容易。

这有点不稳定,因为它假设每个属性都将通过自己的Where子句处理。理论上,没有什么可以停止DataService<T>使用一个Where包含对PartitionKeyand的单个表达式测试的子句RowKey。所以理论上它可以使用:

src.Where(e => e.PartitionKey == "123").Where(e => e.RowKey == "456")

或者

src.Where(e => e.PartitionKey == "123" && e.RowKey == "456")

两者应该具有相同的效果。它碰巧使用了前者,我的代码依赖于它,但我没有找到任何DataService<T>承诺以任何特定形式提供查询的文档。所以我们对DataService<T>. 一个更健壮的实现会想要处理任何一种形式,尽管这感觉很麻烦——理论上我们可以被问到很多方法。目前尚不清楚是否有一种完全通用且安全的方法来制作与我们可能在此处收到的任何查询匹配的新对象。

但是,这是在测试中运行的代码,所以我们会在开发时检测到这种问题,所以我想这是可以接受的。

然而,尽管这使我们能够为 PUT 生成合适的目标,但事实证明这还不够。我们得到DataServiceException以下错误:

由于实体类型“Mm.Web.Tests.Fakes.AzureTableStorage.FakeUserPermission”具有一个或多个 etag 属性,因此必须为该类型的 DELETE/PUT 操作指定 If-Match HTTP 标头。

内部DataService<T>已经决定 PUT 请求必须包含一个 ETag 如果它有任何意义,因为你将如何确定你正在编辑你要编辑的实体?这对于更新是有意义的,但对于插入显然没有意义。所以这对upsert不友好。

我尝试在客户端包含一个 etag:

var permission = new TableEntity(userId, claimId) { ETag = "*" };
await _myTable.ExecuteAsync(TableOperation.InsertOrReplace(permission));

但是,Azure 表存储客户端显然足够聪明,知道 etag 对 upsert 没有意义。(如果你知道 etag,你知道这绝对是一个更新,所以你不应该使用 upsert。)所以它实际上并没有传递那个 ETag。

但是,您可以解决此问题。IUpdatable您可以实现,而不仅仅是实现IDataServiceUpdateProvider,它派生自IUpdatable并添加一个成员:

public void SetConcurrencyValues(
    object resourceCookie,
    bool? checkForEquality,
    IEnumerable<KeyValuePair<string, object>> concurrencyValues)
{
}

如果你实现了这个接口,你基本上就是在告诉DataService<T>你想自己处理 ETags。因为我很高兴没有 ETag,所以什么也不做也没关系。只需提供此接口的空实现即可禁用默认的 ETag 处理,并且我们不再遇到异常。所以它似乎有点工作。

一个问题是,在您生成假替身时,没有明确的方法来区分 PUT 或 DELETE。GetResource没有被告知手术是什么。或者至少,不是直接的。碰巧的是,我发现该fullTypeName论点恰好是null针对 DELETE 的,但文档并没有承诺这一点。所以我觉得我在依赖一个无证的巧合。

而这反过来又像是一个潜在问题的症状:这里涉及的接口并不是为了支持 upsert 而设计的。因此,尽管有可能让它类似于“工作”的东西,但它总是会有点令人不满意。

所以我怀疑唯一令人满意的解决方案是在 HTTP 级别伪造它,因为这是支持我试图伪造的语义的唯一方法。

于 2013-03-08T10:53:53.023 回答