05 July 2011 ASP.NET MVC, HowTo, NoSQL, RavenDB Robert Muehsig

Lange, lange Zeit (jedenfalls für mich Zwinkerndes Smiley ) galt, dass Daten ganz klar in eine Datenbank kommen. Die Daten werden gewöhnlich in einer relationalen Datenbank gespeichert und miteinander verknüpft. Seit einiger Zeit jedoch gibt es “Widerstand” – NoSQL ist das Stichwort.

Was sind NoSQL Datenbanken?

Es gibt verschiedenste Ausprägung von NoSQL Datenbanken – für einen ersten Eindruck ist der Wikipedia Artikel gut.

Prominente Vertreter sind MongoDB sowie CouchDB.

RavenDB?

RavenDB ist ein recht junger Vertreter der Dokumentendatenbanken. Der Quellcode ist Open Source, allerdings benötigt man für eine Closed-Source Anwendung eine Lizenz. Entwickelt ist es von Ayende Rahien, dessen Blog mittlerweile auch mit RavenDB als Datenquelle läuft.

Der größte Unterschied zu den anderen NoSQL Datenbanken liegt vermutlich in der guten Integration auf der Windows & .NET Plattform.

Datenspeicherung mit RavenDB…

image

Anders als bei einer SQL Datenbank werden in RavenDB schemalose JSON Dokumente gespeichert. Es können auch LINQ basierte Indexe angelegt werden. Die Dokumentation ist für einen ersten Einstieg auch recht gut. Insbesondere die “Document Structure Design Considerations” sollten Neulinge in NoSQL sich mal durchlesen.

Einstieg…

imageRavenDB kann über die Download-Seite runtergeladen werden. In diesem Package sind einige Samples, der RavenDB Server und diverse Client-Bibliotheken. Am interessantesten ist der Ordner “Server” und (jedenfalls für den Einstieg) der Ordner “Samples”.

Zum Starten von RavenDB gibt es mehrere Möglichkeiten:

- Als Konsolenanwendung, welche auf Port 8080 lauscht
- Als Webseite im IIS
- Als Windows Dienst

Vermutlich habe ich andere Optionen noch vergessen Zwinkerndes Smiley

Am einfachsten ist allerdings die Konsolenanwendung.

Dazu einfach im Ordner “Server” die “Raven.Server.exe” ausführen (als Admin!) :

image

RavenDB Management Oberfläche

Nach dem Start des Servers sollte sich die Management Oberfläche im Browser öffnen. Das RavenDB Management Studio ist in Silverlight umgesetzt und macht jedenfalls optisch einen netten Eindruck. In dieser Management Oberfläche kann man sich Dokumente anschauen und natürlich bearbeiten. Ich vermute einfach mal, dass es ähnlich ist wie das SQL Management Studio.

imageimage

image

Um RavenDB im Projekt nutzen zu können…

Kann man entweder die Assemblies aus dem Download Ordner nehmen oder man holt sich das NuGet Package.

Kommen wir zum Coding…

Nachdem nun die Konzepte wenigstens im Ansatz angeschnitten wurden und auch der Server läuft kommen wir nun zum Coding. Einige Code-Teile stammen aus dem Blogsystem vom Ayende – genannt Raccoon.

image

Aus dem Raccoon Projekt habe ich etwas Infrastruktur-Code mitgenommen.

Dieser Code erleichtert einfach den Zugriff auf die RavenDB und ist in jedem Controller einfach aufzurufen.

Nachfolgender Code ist “Infrastruktur”-Code. Wahrscheinlich kann man es auch kürzer fassen, aber dann wird es vermutlich nicht so elegant Zwinkerndes Smiley

RavenActionFilterAttribute.cs

Dieses Attribute ist in meinem Beispiel auch als Globaler Filter eingebunden in der Global.ascx

        
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new RavenActionFilterAttribute());
        }

Der Code des Filters:

    /// <summary>
    /// This filter will manage the session for all of the controllers that needs a Raven Document Session.
    /// It does so by automatically injecting a session to the first public property of type IDocumentSession available
    /// on the controller.
    /// </summary>
    public class RavenActionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (filterContext.IsChildAction)
            {
                DocumentStoreHolder.TrySetSession(filterContext.Controller, (IDocumentSession)filterContext.HttpContext.Items[this]);
                return;
            }
            filterContext.HttpContext.Items[this] = DocumentStoreHolder.TryAddSession(filterContext.Controller);
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            if (filterContext.IsChildAction)
                return;
            DocumentStoreHolder.TryComplete(filterContext.Controller, filterContext.Exception == null);
        }
    }

DocumentStoreHelper.cs

Im Grunde wird hier die Verbindung zum Store hergestellt und der Zugriff gewährleistet. Ich bin mir selber nicht ganz sicher, was er sonst noch macht Zwinkerndes Smiley

    /// <summary>
    /// This class manages the state of objects that desire a document session. We aren't relying on an IoC container here
    /// because this is the sole case where we actually need to do injection.
    /// </summary>
    public class DocumentStoreHolder
    {
        private static IDocumentStore documentStore;

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

        private static IDocumentStore CreateDocumentStore()
        {
            var store = new DocumentStore
            {
                ConnectionStringName = "RavenDB"
            }.Initialize();

            return store;
        }

        private static readonly ConcurrentDictionary<Type, Accessors> AccessorsCache = new ConcurrentDictionary<Type, Accessors>();


        private static Accessors CreateAccessorsForType(Type type)
        {
            var sessionProp =
                type.GetProperties().FirstOrDefault(
                    x => x.PropertyType == typeof(IDocumentSession) && x.CanRead && x.CanWrite);
            if (sessionProp == null)
                return null;

            return new Accessors
            {
                Set = (instance, session) => sessionProp.SetValue(instance, session, null),
                Get = instance => (IDocumentSession)sessionProp.GetValue(instance, null)
            };
        }


        public static IDocumentSession TryAddSession(object instance)
        {
            var accessors = AccessorsCache.GetOrAdd(instance.GetType(), CreateAccessorsForType);

            if (accessors == null)
                return null;

            var documentSession = DocumentStore.OpenSession();
            accessors.Set(instance, documentSession);

            return documentSession;
        }

        public static void TryComplete(object instance, bool succcessfully)
        {
            Accessors accesors;
            if (AccessorsCache.TryGetValue(instance.GetType(), out accesors) == false || accesors == null)
                return;

            using (var documentSession = accesors.Get(instance))
            {
                if (documentSession == null)
                    return;

                if (succcessfully)
                    documentSession.SaveChanges();
            }
        }

        private class Accessors
        {
            public Action<object, IDocumentSession> Set;
            public Func<object, IDocumentSession> Get;
        }

        public static void Initailize()
        {
            //RavenProfiler.InitializeFor(DocumentStore,
            //    //Fields to filter out of the output
            //    "Email", "HashedPassword", "AkismetKey", "GoogleAnalyticsKey", "ShowPostEvenIfPrivate", "PasswordSalt", "UserHostAddress");

        }

        public static void TrySetSession(object instance, IDocumentSession documentSession)
        {
            var accessors = AccessorsCache.GetOrAdd(instance.GetType(), CreateAccessorsForType);

            if (accessors == null)
                return;

            accessors.Set(instance, documentSession);
        }
    }

BaseController für alle Controller, welche auf die RavenDB zugreifen wollen

Um in den Controllern nun Zugriff auf die RavenDB zu bekommen, machen wir einen BaseController, welche als Property die IDocumentSession inne hat:

public abstract class BaseController : Controller
    {
        public new IDocumentSession Session { get; set; }
    }

 

Bis hier hin ist der Code auch “Ayende”-geprüft, da er fast so aus dem Raccoon-Blog übernommen wurde – muss also bis hierhin auch gut sein Zwinkerndes Smiley

Ein einfaches Model

Ich hab ein sehr simples Model, welches auch keine “Verbindungen” zu anderen Objekten hat. Das folgt zu einem späteren Zeitpunkt.

public class Word
    {
        public Guid Id { get; set; }
        public string Value { get; set; }
        public string Description { get; set; }
        public string LcId { get; set; }
        public int UpVotes { get; set; }
        public int DownVotes { get; set; }
    }

CRUD mit RavenDB

public class WordsController : BaseController
    {
        public ViewResult Index()
        {
            var words = Session.Query<Word>().ToList();
            return View(words);
        }

        //
        // GET: /Words/Details/5

        public ViewResult Details(Guid id)
        {
            Word word = Session.Load<Word>(id);
            return View(word);
        }

        //
        // GET: /Words/Create

        public ActionResult Create()
        {
            return View();
        } 

        //
        // POST: /Words/Create

        [HttpPost]
        public ActionResult Create(Word word)
        {
            if (ModelState.IsValid)
            {
                Session.Store(word);
                Session.SaveChanges();
                return RedirectToAction("Index");  
            }

            return View(word);
        }
        
        //
        // GET: /Words/Edit/5
 
        public ActionResult Edit(Guid id)
        {
            Word word = Session.Load<Word>(id);
            return View(word);
        }

        //
        // POST: /Words/Edit/5

        [HttpPost]
        public ActionResult Edit(Word word)
        {
            if (ModelState.IsValid)
            {
                Session.Store(word);
                Session.SaveChanges();
                return RedirectToAction("Index");
            }
            return View(word);
        }

        //
        // GET: /Words/Delete/5
 
        public ActionResult Delete(Guid id)
        {
            Word word = Session.Load<Word>(id);
            return View(word);
        }

        //
        // POST: /Words/Delete/5

        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(Guid id)
        {
            Word word = Session.Load<Word>(id);
            Session.Delete(word);
            Session.SaveChanges();
            return RedirectToAction("Index");
        }
    }

Alles in allem – total easy. Mal sehen wie es weitergeht…

Fazit oder TL;DR

Das wäre es erst mal zur Einführung von RavenDB. Wir haben nun eine kleine Infrastruktur, mit der man leicht an die RavenDB rankommt und die API erscheint erstmal recht simpel. Smiley  Der gesamte Soure Code steht auch auf Codeplex im BusinessBingo Repository zur Verfügung.

Weiterführende Links

Definitiv ist der Blog von Ayende zu empfehlen – dort finden sich einige RavenDB Posts. Desweiteren gibt es auch einige RavenDB Tutorials. Die Serie von Rob Ashton ist auch sehr zu empfehlen.


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!