RavenDb:更新非规范化参考属性值

2023-12-23

我已经实现了RavenDB非规范化参考 http://ravendb.net/docs/faq/denormalized-updates图案。我正在努力将静态索引和补丁更新请求连接在一起,以确保在引用的实例值更改时更新我的​​非规范化引用属性值。

这是我的域名:

public class User
{
    public string UserName { get; set; }
    public string Id { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

public class UserReference
{
    public string Id { get; set; }
    public string UserName { get; set; }

    public static implicit operator UserReference(User user)
    {
        return new UserReference
                {
                        Id = user.Id,
                        UserName = user.UserName
                };
    }
}

public class Relationship
{ 
    public string Id { get; set; }
    public UserReference Mentor { get; set; }
    public UserReference Mentee { get; set; }
}

您可以看到 UserReference 包含引用的 User 的 Id 和 UserName。所以现在,如果我更新给定用户实例的用户名,那么我希望所有用户引用中引用的用户名值也更新。为了实现这一点,我编写了一个静态索引和一个补丁请求,如下所示:

public class Relationships_ByMentorId : AbstractIndexCreationTask<Relationship>
{
    public Relationships_ByMentorId()
    {
        Map = relationships => from relationship in relationships
                                select new {MentorId = relationship.Mentor.Id};
    }
}

public static void SetUserName(IDocumentSession db, User mentor, string userName)
{
    mentor.UserName = userName;
    db.Store(mentor);
    db.SaveChanges();

    const string indexName = "Relationships/ByMentorId";
    RavenSessionProvider.UpdateByIndex(indexName,
        new IndexQuery
        {
                Query = string.Format("MentorId:{0}", mentor.Id)
        },
        new[]
        {
                new PatchRequest
                {
                        Type = PatchCommandType.Modify,
                        Name = "Mentor",
                        Nested = new[]
                                {
                                        new PatchRequest
                                        {
                                                Type = PatchCommandType.Set,
                                                Name = "UserName",
                                                Value = userName
                                        },
                                }
                }
        },
        allowStale: false);
}

最后,由于更新未按预期工作,单元测试失败。

[Fact]
public void Should_update_denormalized_reference_when_mentor_username_is_changed()
{
    using (var db = Fake.Db())
    {
        const string userName = "updated-mentor-username";
        var mentor = Fake.Mentor(db);
        var mentee = Fake.Mentee(db);
        var relationship = Fake.Relationship(mentor, mentee, db);
        db.Store(mentor);
        db.Store(mentee);
        db.Store(relationship);
        db.SaveChanges();

        MentorService.SetUserName(db, mentor, userName);

        relationship = db
            .Include("Mentor.Id")
            .Load<Relationship>(relationship.Id);

        relationship.ShouldNotBe(null);
        relationship.Mentor.ShouldNotBe(null);
        relationship.Mentor.Id.ShouldBe(mentor.Id);
        relationship.Mentor.UserName.ShouldBe(userName);

        mentor = db.Load<User>(mentor.Id);
        mentor.ShouldNotBe(null);
        mentor.UserName.ShouldBe(userName);
    }
}

一切运行良好,索引就在那里,但我怀疑这没有返回补丁请求所需的关系,但老实说,我已经没有人才了。你能帮忙吗?

Edit 1

@马特沃伦allowStale=true没有帮助。然而我注意到了一个潜在的线索。

因为这是一个单元测试,所以我使用 InMemory、嵌入式 IDocumentSession -Fake.Db()在上面的代码中。然而,当调用静态索引时,即执行以下操作时UpdateByIndex(...),它使用通用的IDocumentStore,而不是特定的假IDocumentSession。

当我更改索引定义类然后运行单元测试时,索引会在“真实”数据库中更新,并且可以通过 Raven Studio 查看更改。然而,假域名实例(mentor, mentee等)“保存”到 InMemory 数据库中的数据并未存储在实际数据库中(如预期),因此无法通过 Raven Studio 查看。

难道我打电话给UpdateByIndex(...)正在针对不正确的 IDocumentSession(“真实”会话(没有保存的域实例),而不是假会话)运行?

Edit 2- @西蒙

我已经对上面编辑 1 中概述的问题实施了修复,我认为我们正在取得进展。你是对的,我使用的是静态引用IDocumentStore通过RavenSessionProvder。现在情况并非如此。下面的代码已更新为使用Fake.Db()反而。

public static void SetUserName(IDocumentSession db, User mentor, string userName)
{
    mentor.UserName = userName;
    db.Store(mentor);
    db.SaveChanges();

    const string indexName = "Relationships/ByMentorId";
    db.Advanced.DatabaseCommands.UpdateByIndex(indexName,
                                        new IndexQuery
                                        {
                                                Query = string.Format("MentorId:{0}", mentor.Id)
                                        },
                                        new[]
                                        {
                                                new PatchRequest
                                                {
                                                        Type = PatchCommandType.Modify,
                                                        Name = "Mentor",
                                                        Nested = new[]
                                                                {
                                                                        new PatchRequest
                                                                        {
                                                                                Type = PatchCommandType.Set,
                                                                                Name = "UserName",
                                                                                Value = userName
                                                                        },
                                                                }
                                                }
                                        },
                                        allowStale: false);
}
}

您会注意到我还重置了allowStale=false。现在,当我运行这个时,我收到以下错误:

Bulk operation cancelled because the index is stale and allowStale is false

我认为我们已经解决了第一个问题,现在我正在使用正确的 Fake.Db,我们遇到了首先突出显示的问题,即索引已过时,因为我们在单元测试中运行得超快。

现在的问题是:我怎样才能使UpdateByIndex(..)方法等到命令 Q 为空并且索引被认为是“新鲜”?

Edit 3

考虑到防止陈旧索引的建议,我更新了代码如下:

public static void SetUserName(IDocumentSession db, User mentor, string userName)
{
    mentor.UserName = userName;
    db.Store(mentor);
    db.SaveChanges();

    const string indexName = "Relationships/ByMentorId";

    // 1. This forces the index to be non-stale
    var dummy = db.Query<Relationship>(indexName)
            .Customize(x => x.WaitForNonStaleResultsAsOfLastWrite())
            .ToArray();

    //2. This tests the index to ensure it is returning the correct instance
    var query = new IndexQuery {Query = "MentorId:" + mentor.Id};
    var queryResults = db.Advanced.DatabaseCommands.Query(indexName, query, null).Results.ToArray();

    //3. This appears to do nothing
    db.Advanced.DatabaseCommands.UpdateByIndex(indexName, query,
        new[]
        {
                new PatchRequest
                {
                        Type = PatchCommandType.Modify,
                        Name = "Mentor",
                        Nested = new[]
                                {
                                        new PatchRequest
                                        {
                                                Type = PatchCommandType.Set,
                                                Name = "UserName",
                                                Value = userName
                                        },
                                }
                }
        },
        allowStale: false);
}

从上面编号的评论来看:

  1. 放入一个虚拟查询以强制索引等待,直到它不再过时为止。消除了有关过时索引的错误。

  2. 这是一条测试线,以确保我的索引正常工作。看来还好。返回的结果是所提供的 Mentor.Id ('users-1') 的正确关系实例。

    { “导师”: { "Id": "用户-1", "用户名": "导师先生" }, “学员”:{ “Id”:“用户2”, “用户名”:“学员先生” } ... }

  3. 尽管索引未过时并且看起来运行正常,但实际的补丁请求似乎没有执行任何操作。导师非规范化参考中的用户名保持不变。

因此,现在的怀疑落在了补丁请求本身上。为什么这不起作用?这可能是我设置要更新的 UserName 属性值的方式吗?

...
new PatchRequest
{
        Type = PatchCommandType.Set,
        Name = "UserName",
        Value = userName
}
...

您会注意到我只是分配了一个字符串值userName参数直接到Value属性,其类型为RavenJToken。这会是一个问题吗?

Edit 4

极好的!我们有一个解决方案。我重新编写了我的代码,以允许你们提供的所有新信息(谢谢)。以防万一有人真正读到这里,我最好放入工作代码来让他们结束:

单元测试

[Fact]
public void Should_update_denormalized_reference_when_mentor_username_is_changed()
{
    const string userName = "updated-mentor-username";
    string mentorId; 
    string menteeId;
    string relationshipId;

    using (var db = Fake.Db())
    {
        mentorId = Fake.Mentor(db).Id;
        menteeId = Fake.Mentee(db).Id;
        relationshipId = Fake.Relationship(db, mentorId, menteeId).Id;
        MentorService.SetUserName(db, mentorId, userName);
    }

    using (var db = Fake.Db(deleteAllDocuments:false))
    {
        var relationship = db
                .Include("Mentor.Id")
                .Load<Relationship>(relationshipId);

        relationship.ShouldNotBe(null);
        relationship.Mentor.ShouldNotBe(null);
        relationship.Mentor.Id.ShouldBe(mentorId);
        relationship.Mentor.UserName.ShouldBe(userName);

        var mentor = db.Load<User>(mentorId);
        mentor.ShouldNotBe(null);
        mentor.UserName.ShouldBe(userName);
    }
}

假货

public static IDocumentSession Db(bool deleteAllDocuments = true)
{
    var db = InMemoryRavenSessionProvider.GetSession();
    if (deleteAllDocuments)
    {
        db.Advanced.DatabaseCommands.DeleteByIndex("AllDocuments", new IndexQuery(), true);
    }
    return db;
}

public static User Mentor(IDocumentSession db = null)
{
    var mentor = MentorService.NewMentor("Mr. Mentor", "[email protected] /cdn-cgi/l/email-protection", "pwd-mentor");
    if (db != null)
    {
        db.Store(mentor);
        db.SaveChanges();
    }
    return mentor;
}

public static User Mentee(IDocumentSession db = null)
{
    var mentee = MenteeService.NewMentee("Mr. Mentee", "[email protected] /cdn-cgi/l/email-protection", "pwd-mentee");
    if (db != null)
    {
        db.Store(mentee);
        db.SaveChanges();
    }
    return mentee;
}


public static Relationship Relationship(IDocumentSession db, string mentorId, string menteeId)
{
    var relationship = RelationshipService.CreateRelationship(db.Load<User>(mentorId), db.Load<User>(menteeId));
    db.Store(relationship);
    db.SaveChanges();
    return relationship;
}

用于单元测试的 Raven Session Provider

public class InMemoryRavenSessionProvider : IRavenSessionProvider
{
    private static IDocumentStore documentStore;

    public static IDocumentStore DocumentStore { get { return (documentStore ?? (documentStore = CreateDocumentStore())); } }

    private static IDocumentStore CreateDocumentStore()
    {
        var store = new EmbeddableDocumentStore
            {
                RunInMemory = true,
                Conventions = new DocumentConvention
                    {
                            DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites,
                            IdentityPartsSeparator = "-"
                    }
            };
        store.Initialize();
        IndexCreation.CreateIndexes(typeof (RavenIndexes).Assembly, store);
        return store;
    }

    public IDocumentSession GetSession()
    {
        return DocumentStore.OpenSession();
    }
}

索引

public class RavenIndexes
{
    public class Relationships_ByMentorId : AbstractIndexCreationTask<Relationship>
    {
        public Relationships_ByMentorId()
        {
            Map = relationships => from relationship in relationships
                                    select new { Mentor_Id = relationship.Mentor.Id };
        }
    }

    public class AllDocuments : AbstractIndexCreationTask<Relationship>
    {
        public AllDocuments()
        {
            Map = documents => documents.Select(entity => new {});
        }
    }
}

更新非规范化参考

public static void SetUserName(IDocumentSession db, string mentorId, string userName)
{
    var mentor = db.Load<User>(mentorId);
    mentor.UserName = userName;
    db.Store(mentor);
    db.SaveChanges();

    //Don't want this is production code
    db.Query<Relationship>(indexGetRelationshipsByMentorId)
            .Customize(x => x.WaitForNonStaleResultsAsOfLastWrite())
            .ToArray();

    db.Advanced.DatabaseCommands.UpdateByIndex(
            indexGetRelationshipsByMentorId,
            GetQuery(mentorId),
            GetPatch(userName),
            allowStale: false
            );
}

private static IndexQuery GetQuery(string mentorId)
{
    return new IndexQuery {Query = "Mentor_Id:" + mentorId};
}

private static PatchRequest[] GetPatch(string userName)
{
    return new[]
            {
                    new PatchRequest
                    {
                            Type = PatchCommandType.Modify,
                            Name = "Mentor",
                            Nested = new[]
                                    {
                                            new PatchRequest
                                            {
                                                    Type = PatchCommandType.Set,
                                                    Name = "UserName",
                                                    Value = userName
                                            },
                                    }
                    }
            };
}

尝试改变你的线路:

RavenSessionProvider.UpdateByIndex(indexName,  //etc

to

db.Advanced.DatabaseCommands.UpdateByIndex(indexName,  //etc

这将确保在单元测试中使用的同一(假)文档存储上发出更新命令。

编辑2的答案:

使用 UpdateByIndex 时,没有自动方法来等待非过时结果。你有几个选择SetUserName method:

1 - 将数据存储更改为始终立即更新索引,如下所示(注意:这可能会对性能产生不利影响):

store.Conventions.DefaultQueryingConsistency = ConsistencyOptions.MonotonicRead;

2 - 在 UpdateByIndex 调用之前对索引运行查询,指定WaitForNonStaleResults option:

var dummy = session.Query<Relationship>("Relationships_ByMentorId")
.Customize(x => x.WaitForNonStaleResultsAsOfLastWrite())
.ToArray();

3 - 当索引过时时捕获异常抛出,执行Thread.Sleep(100)并重试。

编辑3的答案:

我终于弄清楚了,并通过了测试......不敢相信,但这似乎只是一个缓存问题。当您重新加载文档进行断言时,您需要使用不同的会话...例如

using (var db = Fake.Db())
{
    const string userName = "updated-mentor-username";
    var mentor = Fake.Mentor(db);
    var mentee = Fake.Mentee(db);
    var relationship = Fake.Relationship(mentor, mentee, db);
    db.Store(mentor);
    db.Store(mentee);
    db.Store(relationship);
    db.SaveChanges();

    MentorService.SetUserName(db, mentor, userName);
}

using (var db = Fake.Db())
{
    relationship = db
        .Include("Mentor.Id")
        .Load<Relationship>(relationship.Id);
    //etc...
}

不敢相信我没有早点发现这一点,抱歉。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

RavenDb:更新非规范化参考属性值 的相关文章

随机推荐