Comenzando con ravendb

Instalación o configuración

Instrucciones detalladas sobre cómo configurar o instalar ravendb.

Una sencilla aplicación de consola RavenDB

Para este ejemplo, usaremos la instancia RavenDB de prueba en vivo.

Construiremos una aplicación de consola simple aquí que demuestra las operaciones más básicas:

  • Creación
  • Recuperación por Id.
  • Consulta
  • Actualización
  • Eliminación

Comience creando una nueva solución de Visual Studio y agréguele un proyecto de aplicación de consola. Llamémoslo RavenDBDemoConsole. El uso de la biblioteca del cliente debe ser similar si el lector usa VS Code o su editor favorito.

A continuación, debemos agregar las referencias requeridas. Haga clic con el botón derecho en el nodo Referencias en el panel Explorador de soluciones y seleccione Administrar paquetes NuGet. Busque en línea ‘RavenDb.Client’. Usaré la última versión estable, que es, al momento de escribir este artículo, 3.5.2.

Escribamos algo de código, ¿de acuerdo? Comience agregando las siguientes instrucciones de uso:

using Raven.Client;
using Raven.Client.Document;

Estos nos permiten usar IDocumentStore y DocumentStore de RavenDB, que es una interfaz y su implementación lista para usar para conectarnos a una instancia de RavenDB. Este es el objeto de nivel superior que necesitamos usar para conectarnos a un servidor y la documentación de RavenDB informa que se usa como un singleton en la aplicación.

Así que seguiremos adelante y crearemos uno, pero para simplificar, no implementaremos el envoltorio singleton a su alrededor, simplemente lo desecharemos cuando el programa salga para que la conexión se cierre de manera limpia. Agregue el siguiente código a su método principal:

using (IDocumentStore store = new DocumentStore
{
    Url = "http://live-test.ravendb.net",
    DefaultDatabase = "Pets"
})
{
    store.Initialize();
}

Como se dijo al principio, usamos la Instancia Live Test RavenDB, usamos su dirección como la propiedad Url de DocumentStore. También especificamos un nombre de base de datos predeterminado, en este caso, “Mascotas”. Si la base de datos aún no existe, RavenDB la crea al intentar acceder a ella. Si existe, entonces el cliente puede usar el existente. Necesitamos llamar al método Initialize() para que podamos comenzar a operar en él.

En esta sencilla aplicación, mantendremos dueños y mascotas. Pensamos en su conexión, ya que un dueño puede tener un número arbitrario de mascotas, pero una mascota puede tener solo un dueño. Aunque en el mundo real, una mascota puede tener un número arbitrario de dueños, por ejemplo, un esposo y una esposa, optaremos por esta suposición ya que las relaciones de muchos a muchos en una base de datos de documentos se manejan de manera algo diferente de eso en una base de datos relacional y merece un tema propio. Elegí este dominio porque es lo suficientemente común como para comprenderlo.

Así que ahora deberíamos definir nuestros tipos de objetos de dominio:

public class Owner
{
    public Owner()
    {
        Pets = new List<Pet>();
    }

    public string Id { get; set; }
    public string Name { get; set; }
    public List<Pet> Pets { get; set; }

    public override string ToString()
    {
        return
            "Owner's Id: " + Id + "\n" +
            "Owner's name: " + Name + "\n" +
            "Pets:\n\t" +
            string.Join("\n\t", Pets.Select(p => p.ToString()));
    }
}

public class Pet
{
    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);
    }
}

Hay algunas cosas a tener en cuenta aquí:

En primer lugar, nuestros Owners pueden contener cero o más Pets. Tenga en cuenta que la clase Owner tiene una propiedad llamada Id mientras que la clase Pet no la tiene. Esto se debe a que los objetos Mascota se almacenarán dentro de los objetos Propietario, que es bastante diferente de cómo se implementaría este tipo de relación en una base de datos relacional.

Se puede argumentar que esto no debería implementarse de esta manera, y puede ser correcto, realmente depende de los requisitos. Como regla general, si una ‘Mascota’ tiene sentido que exista sin un ‘Propietario’, entonces no debe incrustarse sino existir por sí sola con un identificador propio. En nuestra aplicación, asumimos que una ‘Mascota’ solo se considera una mascota si tiene un dueño, de lo contrario sería una criatura o una bestia. Por esta razón, no agregamos una propiedad Id a la clase Pet.

En segundo lugar, tenga en cuenta que el identificador de la clase propietaria es una cadena, como generalmente se muestra en los ejemplos de la documentación de RavenDB. Muchos desarrolladores acostumbrados a bases de datos relacionales pueden considerar esto como una mala práctica, lo que normalmente tiene sentido en el mundo relacional. Pero debido a que RavenDB usa Lucene.Net para realizar sus tareas y debido a que Lucene.Net se especializa en operar con cadenas, es perfectamente aceptable aquí. Además, estamos tratando con una base de datos de documentos que almacena JSON y, después de todo, básicamente todo se representa como una cadena. en JSON.

Una cosa más a tener en cuenta sobre la propiedad Id es que no es obligatoria. De hecho, RavenDB adjunta sus propios metadatos a cualquier documento que guardemos, por lo que incluso si no lo definiéramos, RavenDB no tendría problemas con nuestros objetos. Sin embargo, generalmente se define para facilitar el acceso.

Antes de ver cómo podemos usar RavenDB desde nuestro código, definamos algunos métodos auxiliares comunes. Estos deben explicarse por sí mismos.

// Returns the entered string if it is not empty, otherwise, keeps asking for it.
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 pets");
    Console.WriteLine("G - Get an owner with its pets by Owner Id");
    Console.WriteLine("N - Query owners whose name starts with...");
    Console.WriteLine("P - Query owners who have a pet whose name starts with...");
    Console.WriteLine("R - Rename an owner by Id");
    Console.WriteLine("D - Delete an owner by Id");
    Console.WriteLine();
}

Y nuestro método principal:

private static void Main(string[] args)
{
    using (IDocumentStore store = new DocumentStore
    {
        Url = "http://live-test.ravendb.net", 
        DefaultDatabase = "Pets"
    })
    {
        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":
                    QueryOwnersByPetsName(store);
                    break;
                case "R":
                    RenameOwnerById(store);
                    break;
                case "D":
                    DeleteOwnerById(store);
                    break;
                case "Q":
                    break;
                default:
                    Console.WriteLine("Unknown command.");
                    break;
            }
        } while (command != "Q");
    }
}

Creación

Veamos cómo podemos guardar algunos objetos en RavenDB. Definamos los siguientes métodos comunes:

private static Owner CreateOwner()
{
    string name = ReadNotEmptyString("Enter the owner's name.");

    return new Owner { Name = name };
}

private static Pet CreatePet()
{
    string name = ReadNotEmptyString("Enter the name of the pet.");
    string race = ReadNotEmptyString("Enter the race of the pet.");
    string color = ReadNotEmptyString("Enter the color of the pet.");

    return new Pet
    {
        Color = color,
        Race = race,
        Name = name
    };
}

private static void Creation(IDocumentStore store)
{
    Owner owner = CreateOwner();
    Console.WriteLine(
        "Do you want to create a pet and assign it to {0}? (Y/y: yes, anything else: no)", 
        owner.Name);

    bool createPets = Console.ReadLine().ToLower() == "y";
    do
    {
        owner.Pets.Add(CreatePet());

        Console.WriteLine("Do you want to create a pet and assign it to {0}?", owner.Name);
        createPets = Console.ReadLine().ToLower() == "y";
    } while (createPets);

    using (IDocumentSession session = store.OpenSession())
    {
        session.Store(owner);
        session.SaveChanges();
    }
}

Ahora veamos cómo funciona. Hemos definido una lógica simple de C# para crear objetos Owner y seguir creando y asignándoles objetos Pet hasta que el usuario así lo desee. La parte en la que se preocupa RavenDB y, por lo tanto, es el enfoque de este artículo es cómo guardamos objetos.

Para guardar el ‘Propietario’ recién creado junto con sus ‘Mascotas’, primero debemos abrir una sesión que implemente ‘IDocumentSession’. Podemos crear uno llamando a OpenSession en el objeto del almacén de documentos.

Por lo tanto, tenga en cuenta la diferencia, mientras que el almacén de documentos es un objeto permanente que generalmente existe durante toda la vida útil de la aplicación, IDocumentSession es un objeto ligero y de corta duración. Representa una serie de operaciones que queremos realizar de una sola vez (o al menos, en unas pocas llamadas a la base de datos).

RavenDB enfatiza (y de alguna manera fuerza) que evite un número excesivo de viajes de ida y vuelta al servidor, algo que ellos llaman ‘protección de charla Cliente-Servidor’ en el sitio web de RavenDB. Por esta misma razón, una sesión tiene un límite predeterminado de cuántas llamadas a la base de datos tolerará, por lo que se debe prestar atención a cuándo se abre y se elimina una sesión. Debido a que en este ejemplo, tratamos la creación de un ‘Propietario’ y sus ‘Mascotas’ como una operación que debe ejecutarse por sí sola, lo hacemos en una sesión y luego lo desechamos.

Podemos ver dos llamadas a métodos más que son de nuestro interés:

  • session.Store(owner), que registra el objeto para guardarlo y, además, establece la propiedad Id del objeto si aún no está configurada. El hecho de que la propiedad del identificador se denomine Id es por lo tanto una convención.
  • session.Savehanges() envía las operaciones reales para ejecutar al servidor RavenDB, confirmando todas las operaciones pendientes.

Recuperación por ID

Otra operación común es obtener un objeto por su identificador. En el mundo relacional, normalmente hacemos esto usando una expresión ‘Dónde’, especificando el identificador. Pero debido a que en RavenDB, cada consulta se realiza utilizando índices, que pueden ser obsoletos, no es el enfoque a seguir; de hecho, RavenDB lanza una excepción si intentamos consultar por id. En su lugar, deberíamos usar el método Load<T>, especificando el id. Con nuestra lógica de menú ya implementada, solo necesitamos definir el método que realmente carga los datos solicitados y muestra sus detalles:

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

Todo lo relacionado con RavenDB aquí es, una vez más, la inicialización de una sesión y luego usar el método Load. La biblioteca del cliente RavenDB devolverá el objeto deserializado como el tipo que especificamos como parámetro de tipo. Es importante saber que RavenDB no impone ningún tipo de compatibilidad aquí: todas las propiedades asignables se asignan y las no asignables no.

RavenDB necesita el prefijo del tipo de documento antepuesto al ‘Id’; esa es la razón por la que se llama a ‘NormalizeOwnerId’. Si no existe un documento con el Id. especificado, se devuelve null.

Consultando

Veremos dos tipos de consultas aquí: una en la que consultamos las propiedades propias de los documentos Owner y otra en la que consultamos los objetos Pet incrustados.

Comencemos con el más simple, en el que consultamos los documentos Propietario cuya propiedad Nombre comienza con la cadena especificada.

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

Una vez más, porque nos gustaría realizar la consulta como un trabajo independiente, abrimos sesión. Podemos consultar una colección de documentos llamando a Query<TDocumentType> en el objeto de la sesión. Devuelve un objeto IRavenQueryable<TDocumentType>, en el que podemos llamar a los métodos LINQ habituales, así como a algunas extensiones específicas de RavenDB. Hacemos un filtrado simple aquí, y la condición es que el valor de la propiedad Nombre comience con la cadena ingresada. Tomamos los primeros 10 elementos del conjunto de resultados y creamos una lista de ellos. Se debe prestar atención a la especificación adecuada del tamaño del conjunto de resultados: aquí hay otra aplicación defensiva en juego realizada por RavenDB llamada protección ilimitada del conjunto de resultados. Significa que (de forma predeterminada) solo se devuelven los primeros 128 elementos.

Nuestra segunda consulta tiene el siguiente aspecto:

private static void QueryOwnersByPetsName(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.Pets.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();
}

Este no es mucho más complicado, lo he escrito para demostrar cuán naturalmente es posible consultar las propiedades de los objetos incrustados. Esta consulta simplemente devuelve los primeros 10 ‘Propietarios’ que tienen al menos una ‘Mascota’ cuyo nombre comienza con el valor ingresado.

Supresión

Tenemos dos opciones para realizar el borrado. Una es pasar el identificador del documento, que es útil si no tenemos el objeto en la memoria pero tenemos el identificador y nos gustaría evitar un viaje de ida y vuelta a la base de datos que de otro modo se evitaría. La otra forma, obviamente, es pasar un objeto real guardado a RavenDB. Veremos la primera opción aquí, la otra es simplemente usar otra sobrecarga y pasar un objeto apropiado:

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

Una vez más, necesitamos abrir una sesión para realizar nuestro trabajo. Como se mencionó anteriormente, aquí borramos el objeto deseado pasando su identificador al método Delete. El prefijo del identificador también debería estar en su lugar aquí, al igual que en el caso del método Load. Para enviar el comando de eliminación a la base de datos, necesitamos llamar al método SaveChanges, que hará precisamente eso, junto con cualquier otra operación pendiente que esté registrada en la misma sesión.

Actualizando

Y finalmente, veremos cómo actualizar documentos. Básicamente, tenemos dos maneras de hacer esto. El primero es sencillo, cargamos un documento, actualizamos sus propiedades según sea necesario y luego lo pasamos al método Store. Esto debería ser sencillo de acuerdo con la demostración de cargar y guardar, pero hay algunas cosas que vale la pena mencionar.

En primer lugar, la biblioteca del cliente de RavenDB utiliza un rastreador de cambios que hace posible actualizar cualquier documento sin pasarlo a Store siempre y cuando la sesión que cargó el documento aún esté abierta. En este caso, llamar a SaveChanges en la sesión es suficiente para que se realice la actualización.

En segundo lugar, para que esto funcione, el objeto obviamente necesita que se establezca su identificador para que RavenDB pueda averiguar qué actualizar.

Dicho esto, solo echaremos un vistazo a la otra forma de actualización. Existe un concepto llamado aplicación de parches, que se puede utilizar para actualizar documentos. Al igual que sucedió con la eliminación, también tiene sus propios escenarios de uso. Usar el método anterior para realizar una actualización es una buena manera si ya tenemos el objeto en memoria y/o queremos usar su tipo de seguridad. El uso de parches es la opción si queremos evitar un viaje de ida y vuelta innecesario a la base de datos si aún no tenemos el objeto en la memoria. La desventaja es que perdemos parte de la seguridad de tipos, ya que debemos especificar las propiedades para actualizar usando cadenas simples (nada que la magia de LINQ no pueda resolver). Veamos el código:

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

Eso lo envuelve. Debería poder ver el ejemplo funcionando pegando los fragmentos de código en una aplicación de consola.