【问题标题】:How to Load<T> a RavenDB Document, constrained to a Collection, when not using default ID generation strategy如何在不使用默认 ID 生成策略时加载<T> 限制为集合的 RavenDB 文档
【发布时间】:2018-09-29 22:20:11
【问题描述】:

在 RavenDB 4 (v4.0.3-patch-40031) 中,我有两种文档类型:AppleOrange。两者都有相似但也不同的属性。我在运行时的代码中遇到了一个错误,有时会提供 Apple 的 ID,但会返回 Orange。可怕的!

深入研究它,这有点道理。但我正在努力寻找合适的解决方案。

来了。在 RavenDB 中,我将单个 Apple 存储为文档:

id: "078ff39b-da50-4405-9615-86b0d185ba17"
{
    "Name": "Elstar",
    "@metadata": {
        "@collection": "Apples",
        "Raven-Clr-Type": "FruitTest.Apple, FruitTest"
    }
}

为了这个例子,假设我没有Orange 文档存储在数据库中。我希望这个测试能够成功:

// arrange - use the ID of an apple, which does not exist in Orange collection
var id_of_apple = "078ff39b-da50-4405-9615-86b0d185ba17";

// act - load an Orange
var target = await _session.LoadAsync<Orange>("078ff39b-da50-4405-9615-86b0d185ba17");

// assert - should be null, because there is no Orange with that Id
target.Should().BeNull(because: "provided ID is not of an Orange but of an Apple");

...但它失败了。发生的情况是文档 ID 存在,因此 RavenDB 会加载文档。不在乎是什么类型。它会尝试自动映射属性。我预期或错误地假设 Load 类型说明符会将查找限制为该特定文档集合。相反,它在整个数据库中抓取 + 映射它,而不是将其限制为 type &lt;T&gt;。所以行为不同于.Query&lt;T&gt;,后者对收集有约束。

需要注意的是,我使用 guids 作为身份策略,将 Id 设置为 string.Empty(符合 the docs)。我假设默认的 ID 策略,比如entityname/1001,不会有这个问题。

docs on Loading Entities 并没有真正提及这是否是故意的。它只是说:“从数据库下载文档并将它们转换为实体。”。

但是,出于某些原因,我确实希望将 Load 操作限制为单个集合。或者,更好的说法是,尽可能高效地按 ID 从特定集合中加载文档。如果不存在,则返回null。

AFAIK,有两种方法可以实现:

  1. 使用更昂贵的.Query&lt;T&gt;.Where(x =&gt; x.Id == id),而不是.Load&lt;T&gt;(id)
  2. 首先执行.Load&lt;T&gt;(id),然后检查(~不知何故,见底部)它是否是集合 T 的一部分

我的问题可以总结为两个问题

  1. 还有比上述两个选项更高效或更稳定的方法吗?
  2. 如果没有,从两个选项中选择哪一个 - 从性能和稳定性方面推荐?

特别是对于第二个问题,很难正确地正确衡量这一点。至于稳定性,例如没有副作用,我想对 RavenDB 内部结构有更深入了解或经验的人可能会对此有所了解。

注意该问题假定所解释的行为是故意的,而不是 RavenDB 错误。

~不知何故会是:

public async Task<T> Get(string id)
{
    var instance = await _session.LoadAsync<T>(id);
    if (instance == null) return null;

    // the "somehow" check for collection
    var expectedTypeName = string.Concat(typeof(T).Name, "s");
    var actualTypeName = _session.Advanced.GetMetadataFor(instance)[Constants.Documents.Metadata.Collection].ToString();
    if (actualTypeName != expectedTypeName)
    {
        // Edge case: Apple != Orange
        return null;
    }

    return instance;
}

如何重现

2018 年 4 月 19 日更新 - 在有用的 cmets 之后添加了这个可重现的样本(谢谢)。

模型

public interface IFruit
{
    string Id { get; set; }
    string Name { get; set; }
}

public class Apple : IFruit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

public class Orange : IFruit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

测试
例如。在同一个会话中抛出 InvalidCastException(有效),但在第二次它没有。

public class UnitTest1
{
    [Fact]
    public async Task SameSession_Works_And_Throws_InvalidCastException()
    {
        var store = new DocumentStore()
        {
            Urls = new[] {"http://192.168.99.100:32772"},
            Database = "fruit"
        }.Initialize();

        using (var session = store.OpenAsyncSession())
        {
            var apple = new Apple
            {
                Id = Guid.NewGuid().ToString(),
                Name = "Elstar"
            };

            await session.StoreAsync(apple);
            await session.SaveChangesAsync();

            await Assert.ThrowsAsync<InvalidCastException>(() => session.LoadAsync<Orange>(apple.Id));
        }
    }

    [Fact]
    public async Task Different_Session_Fails()
    {
        var store = new DocumentStore()
        {
            Urls = new[] {"http://192.168.99.100:32772"},
            Database = "fruit"
        }.Initialize();

        using (var session = store.OpenAsyncSession())
        {
            var appleId = "ca5d9fd0-475b-41de-a1ab-57bb1e3ce018";

            // this *should* break, because... it's an apple
            // ... but it doesn't - it returns an ORANGE
            var orange = await session.LoadAsync<Orange>(appleId);

            await Assert.ThrowsAsync<InvalidCastException>(() => session.LoadAsync<Orange>(appleId));
        }
    }
}

【问题讨论】:

  • 对我来说它抛出了一个 Cast 异常,你确定它没有失败吗?
  • @Embri 是的,100% 肯定。它不会在抛出错误的意义上失败——它实际上返回了错误的文档类型(映射了属性)。我在 ~Somehow 上添加了一个示例 - 这可能会更好地解释它。同样,请注意 ID 生成策略与默认策略不同。
  • 还需要注意的是,Apples 和 Oranges 具有相似的属性。这也许就是我没有收到 Cast 异常的原因? @Embri
  • 事实上我不明白为什么你没有收到那个异常..Apples 和 Oranges 是同一个基类的子类?
  • 不,它们派生自接口(以 Id 作为属性),但没有基类。刚刚测试过,但 InvalidCastException 仅在您执行 Store + Get 在同一会话中时发生。如果您在新会话中使用现有 ID 执行 LoadAsync,我的问题应该会重现。

标签: ravendb ravendb4


【解决方案1】:

.Query&lt;T&gt;.Where(x =&gt; x.Id == id) 是要走的路。在 RavenDB 4.0 中,按 ID 的查询直接由幕后的文档存储处理(而不是通过索引),因此它与Load 的效率相同。

您的方案的优势在于查询的范围仅限于指定的集合。

【讨论】:

  • 谢谢。我还没有找到in the source 在引擎盖下它实际上是相同的内部结构。也许你可以指出这在幕后发生的地方?此外,在这种情况下,Query 是否可能会自动创建动态索引(我想避免这种情况,因为 Id 已经针对单次提取进行了优化,所以这似乎是不必要的步骤)?
  • "按 ID 查询的处理方式不同。如果查询优化器可以看到您的查询正在使用 ID,它将直接使用其 ID 获取相关文档并传递,而不是通过索引。它们到查询的其余部分进行处理。这适用于您按 ID 查询并且没有额外的过滤器、排序等需要使用索引的情况。 (在 RavenDB 4.0 书中,github.com/ravendb/book/releases
  • 在源代码中,此类查询称为 集合查询,由 CollectionQueryRunner 处理
【解决方案2】:

嗯,我找到了问题所在,但我不明白为什么。

你说:

通过将 Id 设置为 string.Empty

但是在你写的例子中Id = Guid.NewGuid().ToString(); 在我的测试中,我明确分配 string.Empty 并且我得到了强制转换异常,当我将生成的 Guid 分配给实体(就像你一样)时,我重现了你的情况。可能 ravendb 在这两种情况下做出了一些不同的考虑导致了这种行为,我不知道它是否可以被认为是一个错误。

然后使用string.Empty

【讨论】:

  • 是的,你是对的!我的错误是在我更改为Guid.NewGuid() 而不是string.Empty 的行中(正如我所提到的)。改回来确实会抛出 InvalidCastException,这对我来说绝对没问题,所以我不必做古怪的解决方法。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-11
  • 1970-01-01
  • 2013-12-14
  • 2011-06-21
  • 2013-05-31
  • 1970-01-01
  • 2020-02-10
相关资源
最近更新 更多