Table of contents
1.
Introduction
2.
What is RavenDB
2.1.
Characteristics of RavenDB:
3.
Creating a new RavenDB database
3.1.
Creation
3.2.
Retrieval
3.3.
Querying
3.4.
Deletion
3.5.
Updating
4.
FAQs
5.
Key Takeaways
Last Updated: Mar 27, 2024
Easy

RavenDB Implementation

Author Ankit Kumar
1 upvote

Introduction

You've probably heard of NoSQL and how Google and Facebook use non-relational databases to handle their traffic. And, in most cases, that was the end of it. Because scaling relational databases is exceedingly tricky to impossible, NoSQL was created.

Hibernating Rhinos Ltd developed RavenDB, an open-source completely ACID document-oriented database written in C#.

Also see, Multiple Granularity in DBMS

What is RavenDB

RavenDB is a document-based, cross-platform, distributed, ACID-compliant NoSQL database that provides excellent performance while staying relatively simple to use.

Characteristics of RavenDB:

  1. Cross-platform: RavenDB is a database management system that runs on Windows, Linux, and Raspberry Pi. RavenDB can be run on Macs using the Docker container technology. Designing databases and related apps allow developers freedom when designing associated databases and apps.
  2. ACID Compliant: 
    Atomicity assures that every database transaction is viewed as a single entity, no matter how many statements it contains. Atomicity prevents partial updates from causing problems. Transactions succeed or fail as units during processing. If one of the transaction's statements fails, the entire transaction fails. Other database clients will never recognise a partially resolved transaction.
    Consistency ensures that transactions follow all of the database's data validation requirements. The database reverts to the last valid version if a transaction generates non-compliant data.
    Isolation ensures that when many transactions are running simultaneously, they don't interfere with each other and don't try to use data from an in-progress transaction. A collection of concurrent transactions results in the same final database change as if each transaction were executed sequentially.
    Durability prevents the loss of finished transaction data even in post-processing system failures. Completed transaction data is stored in the database system indefinitely, usually in nonvolatile memory.
  3. Distributed database: A distributed database, in general, stores data in numerous physical places (for example, different sites or computers). Clusters are groups of machines that are odd in number, with a minimum of three. A node is a machine in a cluster. Databases can be dispersed across one or more cluster nodes. In a few cases, a whole database may be present on each node in a cluster. Clusters control the distribution of tasks, as well as failure and recovery operations, in addition to data distribution. 
    Compared to typical relational databases, distributed design is also more tolerant of faults. NoSQL databases are ideal for designing mobile applications because of their distributed architecture (see below). Nonetheless, you should be wary of mobile security threats, as 89% of mobile device flaws do not necessitate physical access to the device.
  4. No SQL: The benefits of NoSQL versus SQL are frequently disputed. We can simplify the distinction for our purposes. SQL programming reigns supreme in typical relational databases. NoSQL reigns supreme in non-relational, distributed databases.
    Tables are the foundation of SQL databases. Documents (like RavenDB does), dynamic tables, key-value pairs, and other data types can be used in NoSQL databases.
    To scale horizontally, NoSQL databases rely on a distributed architecture. As the database size increases, it is distributed among numerous cluster nodes. More data necessitates larger servers; therefore, SQL databases scale vertically.
    In NoSQL databases, searches are frequently faster. Whereas SQL database queries require joins, which combine data from numerous tables into a single table, NoSQL queries do not.
  5. Document-based: RavenDB is not limited to storing PDFs or word processing documents because it is document-based. A document is a collection of structured (really semi-structured) self-contained data in a NoSQL database. Extensible Markup Language (XML) and JavaScript Object Notation are two languages that can create documents that will eventually be stored in a NoSQL database (JSON). JSON documents are the most common format used by RavenDB.
    Because all information about an object is stored in a single document instance rather than distributed across numerous tables, document-based databases are often more efficient than relational databases. Because it does not require object-relational mapping, this structure improves database efficiency.

Creating a new RavenDB database

Before proceeding further, I will be assuming that you have the latest version of VS Code, NuGet, installed on your system. 

Here, we'll create a simple console program that demonstrates the most fundamental operations:

  1. Creation
  2. Querying
  3. Updating
  4. Deletion

We'll utilize the Live Test RavenDB instance in this example.

Begin by adding a Console Application project to a new Visual Studio solution. RavenDBDemoConsole is the name we'll give it. If the Reader uses VS Code or another editor, the client library should be used similarly.

After that, we'll need to add the necessary references. Manage NuGet packages by right-clicking the References node in the Solution Explorer pane. Look up 'RavenDb.Client' on the internet.

Let us start by adding the following using statements:

using Raven.Client;

using Raven.Client.Document;

These allow us to connect to a RavenDB instance using RavenDB's IDocumentStore and DocumentStore, an interface, and its out-of-the-box implementation. This is the top-level object that we require to connect to a server, and RavenDB recommends that it be utilized as a singleton in the application.

So we'll make one, but for the sake of simplicity, we won't wrap it in a singleton wrapper; instead, we'll dispose of it when the program ends, ensuring that the connection is closed cleanly. To your main method, add the following code:

using (IDocumentStore store = new DocumentStore

{

       Url = “http://live-test.ravendb.net”,

       DefaultDatabase = “Cars”

})

{

       store.Initialize();

}

As previously stated, we use the Live Test RavenDB Instance and its address as the DocumentStore's Url property. We also give the database a default name, in this case, "Cars." When RavenDB tries to access a database that doesn't exist yet, it creates it. If it already exists, the client can make use of it. We must first execute the Initialize() method to begin working with it.

We'll keep track of owners and Cars with this basic software. We consider their relationship as follows: one owner can have an unlimited number of Cars, but each Car can only have one owner. Even though one Car could have an arbitrary number of owners in the real world, such as a husband and wife, we'll go with this assumption because many-to-many relationships in a document database are handled differently than those in a relational database and require their own discussion. This domain was chosen because it is simple to grasp.

As a result, we'll need to specify our domain object types:

public class Owner

{

    public Owner()

    {

        Cars = new List<Car>();

    }

    public string Id { get; set; }

    public string Name { get; set; }

    public List<Car> Cars { get; set; }

    public override string ToString()

    {

        return

            "Owner's Id: " + Id + "\n" +

            "Owner's name: " + Name + "\n" +

            "Cars:\n\t" +

            string.Join("\n\t", Cars.Select(p => p.ToString()));

    }

}

public class Car

{

    public string Color { get; set; }

    public string Name { get; set; }

    public string Race { get; set; }

    public override string ToString()

    {

        return string.Format("{0}, a {1} {2}", Name, Color, Race);

    }

}

Let us see some of the important things which we need to take care.

To begin, our Owners can have zero or more Cars. It's worth noting that the Owner class has an Id property, but the Car class does not because the Car objects will be kept inside the Owner objects, which is not how a relational database would design this type of relationship.

Some may object that this should not be done this way, and they may be correct; nevertheless, it depends on the requirements. As a general guideline, if a Car makes sense to exist without an Owner, it should not be embedded and should instead exist independently with its identification. We presume in our application that a Car is only a Car if it has an owner; otherwise, it is a creature or a beast. As a result, the Car class does not have an Id property.

Second, keep in mind that the owner class's identification is a string, as illustrated in the RavenDB documentation's samples. Many developers who are used to working with relational databases may think this is a bad approach, yet it makes sense in the relational environment. But, because RavenDB uses Lucene.Net to execute its responsibilities, and Lucene.Net specializes in working with strings, it's okay here - after all, we're dealing with a document database that stores JSON and everything in JSON is a string.

Another thing to remember mind regarding the Id attribute is that it isn't required. RavenDB adds its own information to whatever document we save; thus, RavenDB would have no difficulties with our objects even if we didn't define them. It is, nevertheless, broadly defined to facilitate access.

Let's define a few typical helper methods before looking at how we might use RavenDB from our code. Hopefully, these are self-explanatory.

private static string ReadNotEmptyString(string message)

{

    Console.WriteLine(message);

    string res;

    do

    {

        res = Console.ReadLine().Trim();

        if (res == string.Empty)

        {

            Console.WriteLine("Entered value cannot be empty.");

        }

    } while (res == string.Empty);

 

    return res;

}

// Will use this to prevent text from being cleared before we've read it.

private static void PressAnyKeyToContinue()

{

    Console.WriteLine();

    Console.WriteLine("Press any key to continue.");

    Console.ReadKey();

}

// Prepends the 'owners/' prefix to the id if it is not present (more on it later)

private static string NormalizeOwnerId(string id)

{

    if (!id.ToLower().StartsWith("owners/"))

    {

        id = "owners/" + id;

    }

    return id;

}

// Displays the menu

private static void DisplayMenu()

{

    Console.WriteLine("Select a command");

    Console.WriteLine("C - Create an owner with Cars");

    Console.WriteLine("G - Get an owner with its Cars by Owner Id");

    Console.WriteLine("N - Query owners whose name starts with...");

    Console.WriteLine("P - Query owners who have a Car whose name starts with...");

    Console.WriteLine("R - Rename an owner by Id");

    Console.WriteLine("D - Delete an owner by Id");

    Console.WriteLine();

}

And the main method:

private static void Main(string[] args)

{

    using (IDocumentStore store = new DocumentStore

    {

        Url = "http://live-test.ravendb.net", 

        DefaultDatabase = "Cars"

    })

    {

        store.Initialize();

        string command;

        do

        {

            Console.Clear();

            DisplayMenu();

            command = Console.ReadLine().ToUpper();

            switch (command)

            {

                case "C":

                    Creation(store);

                    break;

                case "G":

                    GetOwnerById(store);

                    break;

                case "N":

                    QueryOwnersByName(store);

                    break;

                case "P":

                    QueryOwnersByCarsName(store);

                    break;

                case "R":

                    RenameOwnerById(store);

                    break;

                case "D":

                    DeleteOwnerById(store);

                    break;

                case "Q":

                    break;

                default:

                    Console.WriteLine("Unknown command.");

                    break;

            }

        } while (command != "Q");

    }

}

Creation

Let's have a look at how to save some objects to RavenDB. Let's go over some of the most prevalent methods:

private static Owner CreateOwner()

{

    string name = ReadNotEmptyString("Enter the owner's name.");

    return new Owner { Name = name };

}

private static Car CreateCar()

{

    string name = ReadNotEmptyString("Enter the name of the Car.");

    string race = ReadNotEmptyString("Enter the race of the Car.");

    string color = ReadNotEmptyString("Enter the color of the Car.");

    return new Car

    {

        Color = color,

        Race = race,

        Name = name

    };

}

private static void Creation(IDocumentStore store)

{

    Owner owner = CreateOwner();

    Console.WriteLine(

        "Do you want to create a Car and assign it to {0}? (Y/y: yes, anything else: no)", 

        owner.Name);

    bool createCars = Console.ReadLine().ToLower() == "y";

    do

    {

        owner.Cars.Add(CreateCar());

        Console.WriteLine("Do you want to create a Car and assign it to {0}?", owner.Name);

        createCars = Console.ReadLine().ToLower() == "y";

    } while (createCars);

    using (IDocumentSession session = store.OpenSession())

    {

        session.Store(owner);

        session.SaveChanges();

    }

}

Let's see how it works now. We've built some simple C# code to create Owner objects and continue to build and assign Car instances until the user requests them. How we preserve objects is the portion that RavenDB is concerned with, hence the topic of this post.

We must first initiate a session that implements IDocumentSession in order to save the newly formed Owner and its Cars. The OpenSession method on the document store object can create one.

RavenDB promotes (and in some cases requires) that you avoid making too many roundtrips to the server, which they refer to as 'Client-Server Chatter Protection' on their website. As a result, a session has a default limit on how many database requests it will tolerate, and it's important to keep track of when a session is created and closed. We accomplish this in one session and then dispose of it because we treat the creation of an Owner and its Cars as an operation that should be conducted on its own in this case.

Two more method calls that we're interested in are as follows:

  • session.Store(owner) registers the object for saving and sets the object's Id property if it isn't already set. As a result, the name Id for the identifier attribute is a convention.
  • session.Save changes() commits all pending operations and sends the real operations to the RavenDB server.

Retrieval

Getting an object by its identification is another typical operation. In relational databases, we usually achieve this by defining the identifier with a Where expression. However, because every query in RavenDB uses indexes, which may be stale, this is not the approach attempting to query by id causes RavenDB to produce an exception. Instead, we should utilize the LoadT> function with the id parameter. We only need to define the function that actually loads the requested data and displays its details now that our menu logic is in place:

private static void GetOwnerById(IDocumentStore store)

{

    Owner owner;

    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to display."));

    using (IDocumentSession session = store.OpenSession())

    {

        owner = session.Load<Owner>(id);

    }

    if (owner == null)

    {

        Console.WriteLine("Owner not found.");

    }

    else

    {

        Console.WriteLine(owner);

    }

    PressAnyKeyToContinue();

}

All that is RavenDB-related here is the initialisation of a session and subsequently the use of the load function. The RavenDB client library will return the deserialized object as the type we provide in the type parameter. It's crucial to note that RavenDB doesn't impose any form of compatibility here; all mappable attributes are mapped, while non-mappable properties aren't.

RavenDB requires the document type prefix to be prepended to the Id, which is why NormalizeOwnerId is called. Null is returned if a document with the supplied Id does not exist.

Querying

We'll see two types of searches here: one that queries the Owner documents' attributes, and another that queries the embedded Car objects.

Let's start with the easiest one: we'll look for Owner records whose Name property begins with the supplied string.

RavenDB requires the document type prefix to be prepended to the Id, which is why NormalizeOwnerId is called. Null is returned if a document with the supplied Id does not exist.

private static void QueryOwnersByName(IDocumentStore store)

{

    string namePart = ReadNotEmptyString("Enter a name to filter by.");

    List<Owner> result;

    using (IDocumentSession session = store.OpenSession())

    {

        result = session.Query<Owner>()

           .Where(ow => ow.Name.StartsWith(namePart))

           .Take(10)

           .ToList();

    }

    if (result.Count > 0)

    {

        result.ForEach(ow => Console.WriteLine(ow));

    }

    else

    {

        Console.WriteLine("No matches.");

    }

    PressAnyKeyToContinue();

}

We open a session once more because we want to conduct the query as a stand-alone task. We may query across a document collection by executing a query on the session object. It returns an IRavenQueryable object, which may be used to invoke standard LINQ methods and RavenDB-specific extensions. We're doing some basic filtering here, and the condition is that the Name property's value must begin with the entered string. We make a list out of the first ten entries in the result set. It's important to pay attention to appropriately selecting the result set size - RavenDB's Unbounded result set protection is a good example of this. Only the first 128 items are returned in this case.

The following is our second query:

private static void QueryOwnersByCarsName(IDocumentStore store)

{

    string namePart = ReadNotEmptyString("Enter a name to filter by.");

    List<Owner> result;

    using (IDocumentSession session = store.OpenSession())

    {

       result = session.Query<Owner>()

           .Where(ow => ow.Cars.Any(p => p.Name.StartsWith(namePart)))

           .Take(10)

           .ToList();

    }

    if (result.Count > 0)

    {

        result.ForEach(ow => Console.WriteLine(ow));

    }

    else

    {

        Console.WriteLine("No matches.");

    }

    PressAnyKeyToContinue();

}

This one isn't much more difficult; I wrote it to show how easily embedded object characteristics can be queried. This query simply returns the first ten Owners who have at least one Car with a name that begins with the value supplied.

Deletion

To delete something, we have two possibilities. The first is to pass the document identifier, which is beneficial if we don't have the object in memory but do have the identifier and want to avoid a roundtrip to the database that would otherwise be unnecessary. The alternative option is to pass a stored object to RavenDB. Here, we'll look at the first approach; the alternative is to just use another overload and pass an appropriate object:

private static void DeleteOwnerById(IDocumentStore store)

{

    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to delete."));

    using (IDocumentSession session = store.

    OpenSession())

    {

        session.

    Delete(id);

        session.

    SaveChanges();

    }

}

We'll need to start a new session to finish our work. As previously stated, we use the Delete method to delete the chosen item by giving it its identifier. The identification prefix should be present here, just as it was with the Load method. We must use the SaveChanges function to send the delete command to the database and any other outstanding operations registered in the same session.

Updating

Finally, we'll look at how to make changes to documents. In general, there are two approaches to this. The first is simple: we load a document, make any necessary changes to its properties, and then give it to the Store method. According to the loading and saving demonstration, this should be simple, but a few things to keep in mind.

For starters, the RavenDB client library makes use of a change tracker, which allows you to alter any document without passing it to Store as long as the session that loaded it is still running. In this situation, simply calling SaveChanges on the session is sufficient to complete the change.

After that, we'll solely look at the other method of updating. Patching is a term that refers to the process of updating documents. It, too, has its own set of applications, just as delete did. If we already have the object in memory and/or wish to use its type safety, using the prior approach to execute an update is a viable option. If we don't already have the object in memory, we can use patching to avoid an otherwise unnecessary roundtrip to the database. The disadvantage is that we lose safety because we have to define the attributes to update using plain strings. Let's look at the code:

private static void RenameOwnerById(IDocumentStore store)

{

    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to rename."));

    string newName = ReadNotEmptyString("Enter the new name.");

    store.DatabaseCommands.Patch(id, new Raven.Abstractions.Data.PatchRequest[]{

        new Raven.Abstractions.Data.PatchRequest

        {

            Name = "Name",

            Value = newName

        }

    });

}

Pheww that all comes to an end. You should be able to see the example in action by this point.

FAQs

  1.  What is the working mechanism of RavenDB?
    RavenDB can be run on a single node (ideal for development or small applications) or in a cluster (which gives you high availability, load balancing, geo-distribution of data and work, etc.). Many databases can be hosted on a single cluster, each of which can span some or all nodes.
  2. How can you define RQL?
    RQL (Raven Query Language) is a query language similar to SQL. When any queries are run, it is utilized to retrieve data from the server. It is specifically built to extract any RavenDB query pipeline from the outside in a straightforward way to understand, utilise, and work with the user.
  3. What are the different ways RavenDB can be started?
    RavenDB can be launched in the following ways. It can be released in the following countries:
    Console mode: This is the best option to use for learning and testing.
    RavenDB is most commonly run in a production setting when hosted by IIS (Internet Information Server).
    RavenDB develops its HTTP server, and processes user queries internally under Windows Service Mode.
    Embedded mode: RavenDB is integrated into the user's application and may run entirely in memory.
  4. What actually is RavenDB?
    RavenDB is a convenient, hassle-free NoSQL implementation that is very fast to use. Generally, we use it on .NET/WINDOWS Platform.
  5. RavenDB is written in which language?
    This oriented database is written in C# and was developed by Hibernating Rhinos Ltd.

Key Takeaways

In this article, we read about RavenDB and its features. Then we dived into an example to create and use our database. We went through creation, updating, querying, and deletion instances.

We hope that this blog has helped you enhance your knowledge regarding RavenDB and if you would like to learn more, check out our articles on Top 100 SQL Problems and if you would like to learn more, check out our articles on Coding Ninjas Studio. Do upvote our blog to help other ninjas grow. Happy Coding!

Live masterclass