Tags

, ,

During the week at work we came across an issue where we are doing a linq query which is using a statement such as this:-

var product = session.Query().Single(GetSource(key));

In Sql Server land we have a product table and the field productcode is unique – we noticed that in RavenDB we were seeing products
which had the same productcode in different documents (not unique), this was not the desired outcome and so we looked into how to go about fixing this in RavenDB, enter the uniqueconstraints RavenDB bundle.

RavendDB bundles are like add-ons and Raven comes with quite a few and they can be exceptionally handy. To use the uniqueconstraints bundle you create a folder and call it plugins, usually you keep this inside the server folder but you can put it anywhere and add a config value so that Raven can find your plugins, an example of how to set up your config to locate plugins:-

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
    <add key="Raven/Port" value="*"/>
    <add key="Raven/DataDir" value="c:\RavenDB\Data"/>
	<add key="Raven/IndexStoragePath" value="c:\RavenDB\Data\Indexes" />
	<add key="Raven/Esent/LogsPath" value="c:\RavenDB\Data\Logs" />
	<add key="Raven/PluginsDirectory" value="c:\RavenDB\Plugins"/>
    <add key="Raven/AnonymousAccess" value="All"/>
	<add key="Raven/ResetIndexOnUncleanShutdown" value="true"/>
  </appSettings>
    <runtime>
		<loadFromRemoteSources enabled="true"/>
		<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
			<probing privatePath="Analyzers"/>
		</assemblyBinding>
	</runtime>
</configuration>

Server side checking

  • To to use the uniqeconstraints bundle add the Raven.Bundles.UniqueConstraints.dll from the bundles folder to your plugins folder where ever you decide to put them.
  • Then restart RavenDB
  • To see if they have been installed and picked up – go to the Raven Management Studio and then at the bottom left hand side, click on statistics – next to where it says Triggers it should say something like Raven.Bundles.UniqueConstraints.UniqueConstraintsDeleteTrigger

Client Side checking

In order to implement the check through code we borrowed the example written by Richard Dingwall taken from his blog.

namespace WhateverNameSpaceYouHave.Extensions.RavenUniqueConstraint
{
    using System;
    using System.Linq.Expressions;

    using Raven.Client;

    public interface IRavenUniqueInserter
    {
        void StoreUnique(IDocumentSession session, T entity, Expression<Func<T, UTunique>>; keyProperty);
    }
}

Aboove the interface, below that code that uses it:-

namespace WhateverNameSpaceYouHave.Extensions.RavenUniqueConstraint
{
    using System;
    using System.Linq.Expressions;

    using Raven.Abstractions.Exceptions;
    using Raven.Client;

    public class RavenUniqueInserter : IRavenUniqueInserter
    {
        public void StoreUnique(IDocumentSession session, T entity, Expression<Func<T, Tunique>> keyProperty)
        {
            if (session == null)
            {
                throw new ArgumentNullException("session");
            }

            if (keyProperty == null)
            {
                throw new ArgumentNullException("keyProperty");
            }

            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            var key = keyProperty.Compile().Invoke(entity).ToString();

            var constraint = new UniqueConstraint { Type = typeof(T).Name, Key = key };

            DoStore(session, entity, constraint);
        }

        private static void DoStore(IDocumentSession session, T entity, UniqueConstraint constraint)
        {
            var previousSetting = session.Advanced.UseOptimisticConcurrency;

            try
            {
                session.Advanced.UseOptimisticConcurrency = true;
                session.Store(constraint, string.Format("UniqueConstraints/{0}/{1}", constraint.Type, constraint.Key));
                session.Store(entity);
                session.SaveChanges();
            }
            catch (ConcurrencyException)
            {
                // rollback changes so we can keep using the session
                session.Advanced.Evict(entity);
                session.Advanced.Evict(constraint);
                throw;
            }
            finally
            {
                session.Advanced.UseOptimisticConcurrency = previousSetting;
            }
        }
    }
}

How to call it from code

 try
                {
                    new RavenUniqueInserter().StoreUnique(session, destination, x =&gt; x.Code);
                    LogManager.GetCurrent().Event(LoggingLevel.Info, "Inserting Product:+" + destination.Code, "Method name here");
                }
                catch (ConcurrencyException)
                {
                    LogManager.GetCurrent().Event(
                        LoggingLevel.Error,
                        "Product code already in use. Code: " + destination.Code, "Method name here");
                }

And thats it, now we wont be able to add a product with a productcode which already exists in our RavenDB database, making it a unique constraint just as youd have in Sql.

Please add a comment or ask a question if you found this useful.