Tutorial MongoDB y C#. Consultas con LINQ

En el pasado artículo de este tutorial de MongoDB y C#, vimos varias maneras de realizar consultas a la base de datos a través del driver de C#. De manera muy sencilla  podíamos realizar consultas pasando directamente un JSON o utilizando la clase Query. No obstante hay una manera mejor, aprovechándonos de uno de las mejores características que tiene .NET: LINQ.

Nota: la útlima iteración de código utilizada en este artículo la podéis encontrar en GitHub. Para las pruebas no existe un conjunto de datos, pero aquí podéis encontrar algunos documentos para que probéis.

Utilizando clases POCO


El driver de MongoDB para C# soporta la utilización de LINQ para realizar todo tipo de consultas. La sintaxis de una consulta LINQ será la siguiente

var collection = database.GetCollection<TDocument>("collectionname");
La clave del asunto, está en el genérico TDocument. El método GetCollection soporta genéricos, por lo que a la hora de usarlo deberemos pasarle el nombre de una clase. En este caso, las clases que utilizaremos como parámetro se conocen como POCO, algo así como Plain Old Class Object, pero que en español suena muy gracioso. Estas clases POCO, no son más que clases simples que solo contienen propiedades, y no contienen métodos, ni implementación de ningún tipo. ¿Y dónde definimos nuestras clases POCO? Como estamos utilizando el patrón modelo-vista-controlador, lo más normal es definir este tipo de elementos en el modelo, ya que son una representación de nuestro modelo de datos. Así que en la carpeta Models de nuestro proyecto vamos a crear un nuevo archivo al que llamaremos DocumentModels.cs. Este archivo contendrá las siguientes clases:

public class CreationInfo
{
  public DateTime creationDate {get;set;}
  public string author {get;set;}
  public string typist {get;set;}
}

public class AccessInfo
{
  public string user {get;set;}
  public string action {get;set;}
  public DateTime accessDate {get;set;}
}

public class VersionInfo
{
  public int mayor {get;set;}
  public int minor {get;set;}
  public string label {get;set;}
  public string path {get;set;}   
} 

public class Document
{
  [BsonElement("id")]
  public ObjectId id { get; set; }
  public string title {get;set;}
  public string subtitle {get;set;}
  [BsonElement("abstract")]
  public string abs {get;set;}
  public string fileFormat {get;set;}
  public CreationInfo creationInfo {get;set;}
  public AccessInfo[] accessHistory {get;set;}
  public VersionInfo[] versions {get;set;}
}

Como podéis ver, estamos definiendo tres clases POCO. La clase principal, llamada Document, contiene diversas propiedades. Entre ellas dos arrays que contienen objetos del tipo AccessInfo y VersionInfo. Lo que haremos con estas clases POCO es pasárselas a MongoDB, que se encargará de convertir los resultados en objetos Document. Al final con estas clases, lo que estamos es definiendo la estructura de un documento de MongoDB, que será lo que se almacene en la base de datos. El driver de C# se encargará de todo el proceso de serialización y deserialización, aunque hay que tener en cuenta algunas cosas.

Lo primero a tener en cuenta, es que todos los campos que devuelva una consulta, deberán tener su equivalente en la clase POCO, salvo que indiquemos lo contrario. Es decir, que si en nuestros documentos de la base de datos existe un campo title, deberá exisitir una propiedad title, y con un tipo equivalente, en la clase.  Si la propiedad no existe, o no tiene un tipo equivalente, el driver devolverá una excepción. Si por alguna razón no queremos añadir todos los campos a la clase POCO, porque quizá no exista en todos los documentos, deberemos hacer uso del atributo de clase [BsonIgnoreExtraElements].

Si el nombre de alguno de los campos de nuestra clase no coincide con el del documento, deberemos utilizar el atributo [BsonElement()]. Por ejemplo en el ejemplo, tenemos definida una propiedad abs ya que abstract es una palabra clave de C# que no podemos utilizar como nombre. Como el campo en MongoDB se llama abstract, hacemos uso del atributo antes mencionado añadiendo un [BsonElement(“abstract”)] justo delante de la propiedad indicada.

Lo mismo hemos hecho con el campo id, que además es del tipo ObjectId, tipo especial definido en las librerías de MongoDB.

Existen otros muchos atributos dentro del espacio de nombres MongoDB.Bson.Serialization.Attributes. Dependiendo de las operaciones que tengamos que realizar, deberemos utilizar unos u otros.  Podéis consultar la lista entera aquí.

Por cierto, en los ejemplos estamos pasando directamente los modelos a las vistas. Lo más correcto sería utilizar ViewModels, pero por simplicidad, eso lo dejaremos para más adelante.

Consultas con LINQ

Ahora que hemos visto como MongoDB serializa y deserializa las clases POCO desde (y hacia) BSON, podemos ver como utilizarlas con LINQ. Para ello hemos creado el siguiente método en nuestra clase MongoDataService:

public string findDocumentsByTitle(string databaseName,string collectionName,string title)
{

    var db = server.GetDatabase(databaseName);
    var documents = db.GetCollection<Document>(collectionName);
           
    var result = from d in documents.AsQueryable<Document>()
                 where d.title == title
                 select d;

    if (result != null)
      return result.ToJson();
    else
      return "{}";
}

El método recibe tres parámetros de tipo string. Los dos primeros hacen referencia a la base de datos y al nombre de la colección que queremos consultar. El último parámetro contiene el título del documento, que será el campo por el que vamos a filtrar.

A diferencia de los ejemplos del anterior artículo, en este caso al método GetCollection le estamos diciendo que queremos convertir los resultados a objetos <Document>. Una vez hemos hecho esto, ya podemos hacer nuestra consulta LINQ, que en este caso utiliza el where para filtrar por el campo title. Para finalizar convertimos los resultados a JSON y devolvemos los datos. Este es el método del controlador que realiza la llamada al método anterior:</p>

[HttpPost]
public ActionResult SearchByTitle(string title)
{
    var connection = WebConfigurationManager.ConnectionStrings["MongoDB"].ToString();
    MongoDataService dataService = new MongoDataService(connection);

    var documents = dataService.findDocumentsByTitle("CylonDM", "Documents", title);

    ViewBag.results = documents;
    ViewBag.typeSearch = "LINQ";
    return View("SearchResults");         
}

El driver de C# para MongoDB, soporta muchos de los operadores habituales de LINQ como son Any, Count, Distinct, First, Last etc. Aquí podéis encontrar una lista de ellos, y una explicación de como usarlos.

Pero a pesar de lo bien que funciona, y de lo bonito que ha quedado, tenemos un problema con nuestro método y es que solo nos servirá para buscar por el campo título. Si queremos consultar por otros campos (o combinación de ellos) tendríamos que crear un método para cada caso, lo cual es bastante aburrido e ineficiente. Para solucionar este problema, podemos uilizar la potencia de las expresiónes Lambda y los genéricos.</p>

Consultas con LINQ y expresiónes Lambda


Lo que buscamos con este método, es generar un método muy genérico, de manera que lo podamos reutilizar para realizar el mayor número de consultas posibles. Y eso podemos conseguir con las expresiones Lambda. Veamos el ejemplo:

public string find&lt;T&gt; (string databaseName, string collectionName, Expression&lt;Func&lt;T, bool&gt;&gt; expression) where T: class { var db = server.GetDatabase(databaseName); var documents = db.GetCollection&lt;T&gt;(collectionName); var result = documents.AsQueryable&lt;T&gt;().Where(expression);

if (result != null)
  return result.ToJson();
else
  return &amp;quot;{}&amp;quot;; } </code>

En este caso uno de los parámetros recibidos por el método, es una expresión Lambda que se resolverá de forma dinámica en tiempo de ejecución. Como veis en ningún momento especificamos el tipo que pasamos como parámetro, ya que los tipos son genéricos y se definirán en el momento de ejecutarse. Veamos como utilizaríamos el método creado para buscar por título, de forma similar a como hacíamos antes:</p>

[HttpPost]
public ActionResult SearchByTitleLambda(string title)
{
    var connection = WebConfigurationManager.ConnectionStrings["MongoDB"].ToString();
    MongoDataService dataService = new MongoDataService(connection);

    var documents = dataService.find<Document>( "CylonDM", "Documents", d=> d.title == title);

    ViewBag.Results = documents;
    ViewBag.TypeSearch = "LINQ y Lambda";
    return View("SearchResults");
}

En este caso utilizamos el método find que hemos definido, pero diciéndole que vamos a buscar objetos <Document>. Lo importante está en el tercer parámetro ya que los dos primeros parámetros del método, de tipo string, son el nombre de la base de datos y la colección a consultar. En el último parámetro, en cambio, estamos pasando una expresión Lambda para filtrar por el campo title.

Lo bueno de este método, es que podríamos usarlo desde cualquier otra parte de la aplicación, pasándole un filtro y una clase POCO totalmente distinta. ¿Qué queremos consultar de una colección que se llama People? Ya tenemos el método creado, solo hay que llamarlo pasándole como parámetro una clase POCO del tipo People.</p>

var documents = dataService.find<People>( "CylonDM", "People", d=> d.name== name && d.age==33);

En el método creado para el ejemplo se usa un parámetro string para pasar el nombre de la colección a consultar, pero podemos adaptar el método para que extraiga el nombre  de forma dinámica través de Reflection y ahorrarnos un parámetro. En este caso, el único requisito sería que las clases poco se tendrían que llamar igual que las colecciones de nuestra base de datos MongoDB.
</p>

Pero no es oro todo lo que reluce

Aunque Linq es una herramienta potente, y podemos usarla junto con MongoDB, hay que tener en cuenta lo que para mi es un problema.

Si utilizamos Linq, las proyecciones de datos se realizan en el cliente. Es decir, que aunque hagamos un Select de un par de campos, MongoDB se trae del servidor todos los datos y luego hace la proyección. Repito: si queremos hacer un Select de, por ejemplo, el _id, nos estaremos trayendo todos los datos de la base de datos para descartarlos y quedarnos con uno. ¿Feo verdad?

Por tanto si tenemos documentos con muchos campos, o devolvemos muchos resultados, nos estaremos trayendo del servidor un montón de bytes inútiles.

Esto sucede con el driver actual, que a día de hoy es el 1.9.2. Tampoco entiendo muy bien porque no han implementado la opción en Linq, ya que la clase MongoCursor ya es capaz de definir las proyecciones con el método SetFields.

Así que mucho ojo con utilizar Linq.

Conclusiones


El driver de C# de MongoDB nos proporciona una ventaja que no tienen los drivers de otras plataformas: podemos utilizar LINQ para realizar consultas. Si además utilizamos la potencia de las expresiones Lambda, seremos capaces de aislar completamente nuestra clase de acceso a datos. Así, si queremos hacer cambios en la implementación de la misma, nos será posible evitar realizar cambios en los métodos de los controladores. En próximas entregas, veremos como aislar completamente nuestra clase de acceso a datos, creando unos cuántos métodos genéricos de consulta.



Recuerda que puedes ver el índice del tutorial y acceder a todos los artículos de la serie desde aquí.



¿Quiéres que te avisemos cuando se publiquen nuevas entradas en el blog?

Suscríbete por correo electrónico o por RSS