Как загрузить<T> документ RavenDB, ограниченный коллекцией, если не используется стратегия генерации идентификатора по умолчанию
В RavenDB 4 (v4.0.3-patch-40031) у меня есть два типа документов: Apple
а также Orange
, Оба имеют схожие, но отличающиеся свойства. Я сталкиваюсь с ошибкой в моем коде во время выполнения, когда иногда предоставляется идентификатор Apple, но возвращается 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");
... но это не удается. Что происходит, так это то, что идентификатор документа существует, поэтому RavenDB загружает документ. Неважно, что это за тип. И он пытается сопоставить свойства автоматически. Я ожидал или предположил, что спецификатор типа загрузки ограничит поиск этой конкретной коллекцией документов. Вместо этого он захватывает + отображает его по всей базе данных, не ограничивая его type <T>
, Так что поведение отличается от .Query<T>
, что делает ограничение на сбор.
Важно отметить, что я использую направляющие как стратегию идентификации, устанавливая Id в
string.Empty
(соответствуют документам). Я предполагаю стратегию идентификатора по умолчанию, которая похожаentityname/1001
, не было бы этой проблемы.
В документах по загрузке сущностей не упоминается, является ли это намеренным или нет. В нем только сказано: "скачать документы из базы данных и преобразовать их в сущности".
Однако по ряду причин я хочу ограничить операцию Load одной коллекцией. Или, лучше сказать, максимально эффективно загружать документ по идентификатору из определенной коллекции. И если он не существует, вернуть ноль.
AFAIK, есть два варианта для достижения этой цели:
- Используйте более дорогой
.Query<T>.Where(x => x.Id == id)
, вместо.Load<T>(id)
- Сделать
.Load<T>(id)
сначала, а затем проверьте (~ как-то, см. внизу), является ли он частью коллекции T
Моя проблема может быть обобщена в двух вопросах:
- Есть ли другой, более производительный или стабильный способ, чем два вышеупомянутых варианта?
- Если нет, то из двух вариантов - что рекомендуется с точки зрения производительности и стабильности?
Особенно по второму вопросу, очень трудно правильно измерить это правильно. Что касается стабильности, например, отсутствия побочных эффектов, я думаю, что кто-то, обладающий более глубокими знаниями или опытом работы с внутренностями RavenDB, может пролить некоторый свет.
NB. Вопрос предполагает, что описанное поведение является преднамеренным, а не ошибкой 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));
}
}
}
2 ответа
Ну, я нашел, в чем проблема, но я не понимаю, почему.
вы сказали:
установив Id в string.Empty
но в примере вы написали Id = Guid.NewGuid().ToString()
; в моих тестах я явно назначаю string.Empty
и я получаю исключение приведения, когда я назначил сгенерированный Guid сущности (как вы), я воспроизвел ваши ситуации. Вероятно, ravendb делает несколько разных соображений в этих двух случаях, которые создают такое поведение, я не знаю, можно ли это считать ошибкой.
Тогда используйте string.Empty
.Query<T>.Where(x => x.Id == id)
это путь В RavenDB 4.0 запросы по идентификатору обрабатываются непосредственно хранилищем документов под обложками (а не индексом), так что это так же эффективно, как Load
,
Преимущество вашего сценария заключается в том, что запросы ограничиваются только указанной коллекцией.