SQLite: Primeras impresiones

Como ya avancé hace poco, los problemas con la instalación de SQL Express me están llevando a considerar el uso de bases de datos embebidas para una aplicación que desarrollo en mi trabajo.

Tras una primera fase de pruebas preliminares con Sql Server CE, SQLite y Firebird (todavía tenemos pendiente VistaDB), la base de datos que más nos está gustando es SQLite.

Por desgracia, nada es perfecto y también hemos encontrados algunas pegas con SQLite. En este post voy a intentar resumir lo que me gusta y lo que no, que luego la memoria es traicionera.

Una de las cosas que más me ha gustado de mi (escasa) experiencia con SQLite es el despliegue. SQLite es una base de datos embebida. Tanto, que para usarla en una aplicación .NET basta con referenciar un único assembly. Esto ya representa una ventaja frente a Sql Server CE o Firebird, que requieren desplegar más archivos.

Para empezar a trabajar con SQLite basta con descargar alguno de los paquetes de System.Data.SQlite que encontraremos en su página. La forma en que lo tienen planteado es muy interesante: en un único assembly se incluye la parte de .NET (la implementación del driver para ADO.NET) y el propio motor de base de datos programado en C (nativo). Esto es posible gracias a la magia de los assemblies en mixed mode, que pueden contener a la vez código manejado y código nativo.

OJO: la mezcla de código manejado y código nativo implica que tenemos que tener en cuenta la arquitectura para la que vamos a compliar (32 o 64 bits).

Una vez que tenemos referenciado ese assembly desde nuestro proyecto, podemos empezar a utilizar las versiones de IDbConnection, IDbCommand, etc, a las que estamos acostumbrados desde ADO.NET. Sin embargo, en nuestro caso, y dado que estamos usando NHibernate, ni siquiera nos hará falta eso, porque SQLite funciona muy bien con NHibernate.

El rendimiento que hemos obtenido en las primeras pruebas con SQLite ha sido bastante bueno (sobre todo comparado con Sql Server CE), permitiéndonos ejecutar la misma aplicación que antes trabajaba con SQL Express manteniendo un rendimiento más que aceptable (al menos para el volumen de datos que manejamos).

ACTUALIZACIÓN (26/07/2011): Después de hacer pruebas de carga con una base datos más realista, el rendimiento no es tan bueno como parecía, especialmente cuando se trata de lanzar consultas que contienen varios left outer join.

El dialecto de SQL usado por SQLite es lo bastante parecido al de SQL Server como para que la mayoría de las consultas específicas que teníamos escritas para SQL haya funcionado sin cambios, o con cambios mínimos (por ejemplo, el top de T-SQL se convierte en un limit en SQLite.

Donde hemos empezado a tener problemas (y aún no sabemos muy bien como resolverlos), ha sido con los tipos de datos soportados por SQLite. Esto merece un análisis más profundo, pero basta con decir que SQLite tiene un tipado dinámico para las columnas. De hecho, el tipado se aplica a nivel de celda y no ha nivel de columna, por lo que una misma columna puede contener a la vez datos de tipo INTEGER y TEXT. Esto no supondría un gran problema (dada la capa de abstracción que nos proporciona NHibernate), si no fuese porque en SQLite no existe el tipo decimal de precisión arbitraria como tal, sino que se almacena como un número real en coma flotante.

¿Qué tiene de malo almacenar los decimal como double? Pues cualquiera que haya intentado trabajar alguna vez con cantidades e importes lo habrá notado. Un double (o en general, cualquier representación en coma flotante), no tiene precisión absoluta, sino que depende de la capacidad de representar en binario el número que queremos almacenar. Esto provoca que, por ejemplo, al intentar almacenar como precio de un producto 1.95€, lo que en realidad estamos almacenando es 1.9499999. Esto conlleva errores de redondeo a la hora de realizar cálculos, y dependiendo del tipo de aplicación, estos errores pueden no ser asumibles.

En nuestro caso, para intentar paliar la situación, hemos obtado por almacenar los decimal utilizando una representación en punto fijo, es decir, hemos decido que almacenarlos como un entero en el que los últimos N dígitos se corresponden con las posiciones decimales. Así, si usamos 3 dígitos para guardar las posiciones decimales, el valor 1.95 quedaría almacenado en la base de datos como 1950. Esto obliga a realizar la conversión en dos puntos, al guardar en la base de datos y al cargar de la base de datos. Por suerte, estamos utilizando NHibernate, y eso nos ha permitido crear un IUserType que se encarga de hacer la conversión por nosotros.

Partiendo de la clase base de Darrel Mozingo, nuestro UserType queda bastante sencillo:

public class FixedPointUserType : BaseImmutableUserType<decimal>
{
    // Factor de conversión entre Int y Decimal. En este caso usamos 3 decimales por lo que 
    // hay que multiplicar o dividir por 1000
    private const decimal CONVERSION_FACTOR = 1000m;
    private const int DECIMAL_PLACES = 3;

    public override object NullSafeGet(IDataReader rs, string[] names, object owner)
    {
        var value = NHibernateUtil.Int64.NullSafeGet(rs, names[0]);
        return value == DBNull.Value ? (object)DBNull.Value : Convert.ToDecimal(value) / 1000.m;
    }
 
    public override void NullSafeSet(IDbCommand cmd, object value, int index)
    {
        object valueToSet;
 
        if (value != null)
            valueToSet = (long)Math.Round(Convert.ToDecimal(value) * CONVERSION_FACTOR, DECIMAL_PLACES);
        else
            valueToSet = DBNull.Value;
 
        NHibernateUtil.Int64.NullSafeSet(cmd, valueToSet, index);
    }
 
    public override SqlType[] SqlTypes
    {
        get { return new[] { SqlTypeFactory.Int64 }; }
    }
}

Pese a todo, aún tenemos puntos por resolver, como por ejemplo la carga de datos desde SQLite cuando usamos proyecciones, o algunas consultas que realizan calculos directamente en la consulta a SQLite. El caso de las proyecciones se nos presenta en situaciones como esta:

return session.CreateQuery("select p.Id, p.Name, p.Price from Product p where p.IsActive = true")
       .SetResultTransformer(Transformers.AliasToBean(typeof(ProductForSale)))
       .ToList();

Para esos casos, creo que sería posible crear una implementación de IResultTransformer que tuviera en cuenta la conversión de enteros representando decimales en punto fijo a decimales al cargar los datos.

Para el caso en que una consulta realiza cálculos directamente en la base de datos, de momento no tenemos ninguna solución, así que nos tocaría duplicar las consultas y mantener la versión para SQL Express y la versión para SQLite.

2 comentarios en “SQLite: Primeras impresiones

  1. Pingback: Modificar la configuración de NHibernate por código | Koalite's blog

  2. Pingback: ¿Hacen los frameworks tontos a los desarrolladores? | Koalite's blog

Comentarios cerrados.