3

Their documentation doesn't clarify this and ChatGPT always give me incorrect code (probably from legacy versions of RavenDB)

I have been using the code below to set the unique constraint, but the unique constraint is not being set even though this is creating a new index. Im using GoLang.

index := ravendb.NewIndexCreationTask("UniqueEmailIndexTask")
    index.Map = "from user in docs.Users select new { user.Email }"

    emailUniqueIndexDef := index.CreateIndexDefinition()
    emailUniqueIndexDef.Name = "UniqueEmailIndex"
    emailUniqueIndexDef.Fields = map[string]*ravendb.IndexFieldOptions{
        "Email": {
            Indexing: ravendb.FieldIndexingExact,
            Storage:  ravendb.FieldStorageNo,
        },
    }

    err := store.Maintenance().Send(ravendb.NewPutIndexesOperation(emailUniqueIndexDef))
Shyam
  • 633
  • 7
  • 13

2 Answers2

3

So i was asked to refer to the RavenDb documentation when I seeked for answer in their github Q&A section. RavenDB doesn't have the concept of a "unique constraint" (atleast not from version 5). So all this time, i was searching for the wrong keywords "Unique constraint" in their documentation, when it was just different concepts and terminologies.

RavenDb has the features CompareExchangeOperation or atomic ClusterWideTransactions that we can use to store unique field values.

Since cluster-wide transactions feature are not yet in their golang client package as of Aug2023 (they promised to ship it with ravenDb 6.0 go-client), i resorted to use it through the CompareExchange method, which also kind of "indexes" the user's email address uniquely in the "CompareExchange" storage.

So referring to their doc, specifically this section https://ravendb.net/docs/article-page/5.4/csharp/client-api/operations/compare-exchange/overview, I got it working use the following code in Golang.

op, _ := ravendb.NewPutCompareExchangeValueOperation("emails/"+*user.Email, user.ID, 0)
err = store.Operations().Send(op, nil)
if err != nil {
   return nil, fmt.Errorf("r.store.Operations().Send(op, nil) failed with %s", err)
}
if !op.Command.Result.IsSuccessful {
   return nil, fmt.Errorf("User with that email already exists")
}

Please note: This stores "emails/user-unique-email@gmail.com" in the CompareExchange storage, and even if you delete the User document, the user wont be able to re-register with the deleted email id, unless you remove the email entry from CompareExchange storage.

Shyam
  • 633
  • 7
  • 13
  • For those who want to set it via ClusterWideTransactions method, pls refer to @AyendeRahien 's answer. Its the recommended way, but as of creating this Stackoverflow Question, ClusterWideTransactions is not supported in GoLang client yet. – Shyam Aug 24 '23 at 08:52
1

@Shyman answer is correct, for Go, but I wanted to point out how that would work in C#, since we did a lot of work to simply the story.

See: https://ayende.com/blog/194405-a/ravendb-5-2-simplifying-atomic-cluster-wide-transactions

With the C# client (and soon across all clients), you'll be able to write it like this:

using var session = store.OpenAsyncSession(new SessionOptions
{
    TransactionMode = TransactionMode.ClusterWide
});

var user = new User{Name = "Ayende"};
await session.StoreAsync(user);
await session.StoreAsync(new {ReservedFor = user.Id}, "usernames/" + user.Name);

await session.SaveChangesAsync();

The reason we need a cluster-wide transaction to ensure uniqueness is that RavenDB is a distributed multi-primary database. Which means that we allow writes on multiple nodes at the same time.

There are mechanisms in place to handle & resolve conflicts in such a scenario, but for uniqueness (where we assume that we have to protect against explicit race conditions or even malicious users), we have to go with a cluster wide transaction to ensure consistency.

Note that other databases would either not run distributed or require a cluster wide transaction anyway, but across all operations, so it isn't visible.

Ayende Rahien
  • 22,925
  • 1
  • 36
  • 41
  • Yes, i opted for compare-exchange method because cluster-wide trans isn't available for Go client yet. I talked with their CEO and he said, it will be supported soon, with ravendb 6.0. But anyway yeah C# and Java clients already support this. – Shyam Aug 24 '23 at 08:49