如何在不使用默认 ID 生成策略时加载 <T> 一个 RavenDB 文档,限制为一个集合
How to Load<T> a RavenDB Document, constrained to a Collection, when not using default ID generation strategy
在 RavenDB 4 (v4.0.3-patch-40031) 中,我有两种文档类型:Apple
和 Orange
。两者具有相似但又截然不同的属性。我 运行 在 运行 时在我的代码中发现了一个错误,有时会提供 Apple 的 ID,但 Orange 是 returned。可怕的!
深究一下,多少有些道理。但我正在努力寻找合适的解决方案。
开始了。在 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 加载文档。不关心它是什么类型。它会尝试自动映射属性。我预计或错误地假设加载类型说明符会将查找限制为特定文档集合。相反,它在整个数据库中抓取 + 映射它,而不是将它限制为 type <T>
。所以行为不同于 .Query<T>
,后者对集合进行约束。
Important to note is that I'm using guids as identity strategy, by setting the Id to string.Empty
(conform the docs). I assume the default ID strategy, which is like entityname/1001
, would not have this issue.
docs on Loading Entities 并没有真正提及这是有意还是无意。它只说:“从数据库下载文档并将它们转换为实体。”。
但是,出于某些原因,我确实想将 Load 操作限制为单个集合。或者,更好地说,尽可能高效地从特定集合中按 ID 加载文档。如果不存在,return null.
据我所知,有两种选择可以实现:
- 使用更昂贵的
.Query<T>.Where(x => x.Id == id)
,而不是 .Load<T>(id)
- 先做
.Load<T>(id)
然后检查(~不知何故,见底部)它是否是集合 T 的一部分
我的问题可以概括为两个问题:
- 有没有比上面提到的两个选项更高效或更稳定的方法?
- 如果没有,在这两个选项中 - 就性能和稳定性而言,哪个是推荐的?
尤其是第二个问题,很难正确衡量。至于稳定性,例如没有副作用,我猜对 RavenDB 内部结构有更深入了解或经验的人可能会对此有所了解。
N.B。该问题假定所解释的行为是故意的,而不是 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/04/19 - 在有帮助的评论后添加了这个可重现的示例(感谢)。
型号
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));
}
}
}
.Query<T>.Where(x => x.Id == id)
是要走的路。在 RavenDB 4.0 中,按 ID 的查询直接由隐藏的文档存储处理(而不是索引),因此它与 Load
一样高效。
您的方案的优点是查询范围仅限于指定的集合。
嗯,我发现应该是什么问题,但我不明白为什么。
你说:
by setting the Id to string.Empty
但是在你写的例子中Id = Guid.NewGuid().ToString()
;
在我的测试中,我明确分配了 string.Empty
并且我得到了转换异常,当我将生成的 Guid 分配给实体(比如你)时,我重现了你的情况。可能 ravendb 在这两种情况下做出了一些不同的考虑,导致了这种行为,我不知道它是否可以被视为一个错误。
然后使用string.Empty
在 RavenDB 4 (v4.0.3-patch-40031) 中,我有两种文档类型:Apple
和 Orange
。两者具有相似但又截然不同的属性。我 运行 在 运行 时在我的代码中发现了一个错误,有时会提供 Apple 的 ID,但 Orange 是 returned。可怕的!
深究一下,多少有些道理。但我正在努力寻找合适的解决方案。
开始了。在 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 加载文档。不关心它是什么类型。它会尝试自动映射属性。我预计或错误地假设加载类型说明符会将查找限制为特定文档集合。相反,它在整个数据库中抓取 + 映射它,而不是将它限制为 type <T>
。所以行为不同于 .Query<T>
,后者对集合进行约束。
Important to note is that I'm using guids as identity strategy, by setting the Id to
string.Empty
(conform the docs). I assume the default ID strategy, which is likeentityname/1001
, would not have this issue.
docs on Loading Entities 并没有真正提及这是有意还是无意。它只说:“从数据库下载文档并将它们转换为实体。”。
但是,出于某些原因,我确实想将 Load 操作限制为单个集合。或者,更好地说,尽可能高效地从特定集合中按 ID 加载文档。如果不存在,return null.
据我所知,有两种选择可以实现:
- 使用更昂贵的
.Query<T>.Where(x => x.Id == id)
,而不是.Load<T>(id)
- 先做
.Load<T>(id)
然后检查(~不知何故,见底部)它是否是集合 T 的一部分
我的问题可以概括为两个问题:
- 有没有比上面提到的两个选项更高效或更稳定的方法?
- 如果没有,在这两个选项中 - 就性能和稳定性而言,哪个是推荐的?
尤其是第二个问题,很难正确衡量。至于稳定性,例如没有副作用,我猜对 RavenDB 内部结构有更深入了解或经验的人可能会对此有所了解。
N.B。该问题假定所解释的行为是故意的,而不是 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/04/19 - 在有帮助的评论后添加了这个可重现的示例(感谢)。
型号
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));
}
}
}
.Query<T>.Where(x => x.Id == id)
是要走的路。在 RavenDB 4.0 中,按 ID 的查询直接由隐藏的文档存储处理(而不是索引),因此它与 Load
一样高效。
您的方案的优点是查询范围仅限于指定的集合。
嗯,我发现应该是什么问题,但我不明白为什么。
你说:
by setting the Id to string.Empty
但是在你写的例子中Id = Guid.NewGuid().ToString()
;
在我的测试中,我明确分配了 string.Empty
并且我得到了转换异常,当我将生成的 Guid 分配给实体(比如你)时,我重现了你的情况。可能 ravendb 在这两种情况下做出了一些不同的考虑,导致了这种行为,我不知道它是否可以被视为一个错误。
然后使用string.Empty