5

RavenDB Denormalized Referenceパターンを実装しました。参照されるインスタンスの値が変更されたときに、非正規化された参照プロパティの値が確実に更新されるようにするために必要な、静的インデックスとパッチ更新要求を結び付けるのに苦労しています。

ここに私のドメインがあります:

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 が含まれていることがわかります。したがって、特定の User インスタンスの UserName を更新すると、すべての UserReferences で参照されている 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);
}

最後に、更新が期待どおりに機能していないために失敗する UnitTest です。

[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);
    }
}

すべてが正常に動作し、インデックスはそこにありますが、これはパッチ リクエストで必要な関係を返していないのではないかと思いますが、正直なところ、私は才能を使い果たしました。助けていただけますか?

編集 1

@MattWarrenは役に立ちallowStale=trueませんでした。しかし、私は潜在的な手がかりに気づきました。

これは単体テストであるため、Fake.Db()上記のコードでは、IDocumentSession を埋め込んだ InMemory を使用しています。ただし、静的インデックスが呼び出されるとき、つまり を実行するときUpdateByIndex(...)は、特定の偽の IDocumentSession ではなく、一般的な IDocumentStore が使用されます。

インデックス定義クラスを変更して単体テストを実行すると、「実際の」データベースでインデックスが更新され、Raven Studio で変更を確認できます。ただし、InMemory データベースに「保存」された偽のドメイン インスタンス (mentorなどmentee) は (予想どおり) 実際のデータベースには保存されないため、Raven Studio からは表示できません。

への呼び出しがUpdateByIndex(...)、偽の IDocumentSession ではなく、「本物の」(ドメイン インスタンスが保存されていない) 誤った IDocumentSession に対して実行されている可能性がありますか?

編集 2 - @Simon

上記の編集 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 が空になり、インデックスが「新しい」と見なされるまでメソッドを待機させるにはどうすればよいですか?

編集 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') の正しい Relationship インスタンスです。

    { "Mentor": { "Id": "users-1", "UserName": "Mr. Mentor" }, "Mentee": { "Id": "users-2", "UserName": "Mr. Mentee" " } ... }

  3. インデックスが古くなく、一見正しく機能しているにもかかわらず、実際のパッチ リクエストは何もしていないように見えます。メンターの非正規化参照の UserName は変更されません。

したがって、疑惑はパッチリクエスト自体にかかっています。なぜこれが機能しないのですか?UserName プロパティの値を更新するように設定している方法でしょうか?

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

type のプロパティにuserName直接 paramの文字列値を代入しているだけであることに気付くでしょう。これは問題になる可能性がありますか?ValueRavenJToken

編集 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", "mentor@email.com", "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", "mentee@email.com", "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 セッション プロバイダー

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
                                            },
                                    }
                    }
            };
}
4

1 に答える 1

4

行を変更してみてください:

RavenSessionProvider.UpdateByIndex(indexName,  //etc

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

これにより、単体テストで使用しているのと同じ (偽の) ドキュメント ストアに対して Update コマンドが発行されるようになります。

編集への回答2:

UpdateByIndex を使用する場合、古くない結果を自動的に待機する方法はありません。SetUserNameメソッドにはいくつかの選択肢があります。

1 - データストアを変更して、次のように常にインデックスをすぐに更新します (注: これはパフォーマンスに悪影響を与える可能性があります)。

store.Conventions.DefaultQueryingConsistency = ConsistencyOptions.MonotonicRead;

2 - UpdateByIndex 呼び出しの直前に、次のWaitForNonStaleResultsオプションを指定して、インデックスに対してクエリを実行します。

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

3 - インデックスが古い場合にスローされる例外をキャッチし、aThread.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...
}

これにもっと早く気づかなかったなんて信じられない、ごめん。

于 2012-04-24T13:08:56.043 に答える