Escribir la clase de controlador genérica para cualquier soporte de base de datos

Durante los últimos días, estuve trabajando con varias bases de datos como MySQL, Oracle, Ibmdb2, etc. que se conectan con dot net a través de proveedores odbc.

Por ejemplo:

1)MySQL: Driver={MySQL ODBC 5.1 Driver};server=**********;uid=**;database=**;port=***;pwd=***;" 2)oracle: Driver={Microsoft ODBC for Oracle};server=**********;uid=**;database=**;port=***;pwd=***;" 3)Db2: Driver={IBM DB2 ODBC DRIVER};server=**********;uid=**;database=**;port=***;pwd=***;" 

ahora mi pregunta es

es posible escribir clases genéricas para cualquier proveedor de base de datos como

 Driver={My own driver};server=**********;uid=**;database=**;port=***;pwd=***;" 

que conecta cada base de datos simplemente cambiando el nombre del controlador en web.config y colocando ese archivo dll en la carpeta bin de mi aplicación web publicada o proyecto de sitio web.

Lanzar uno propio no es tan importante. Aquí hay una estructura básica de cómo lo implementaría para las necesidades mínimas (por supuesto, puede ampliarlo):

1) Primero crea una interfaz que especifique las funcionalidades básicas.

 interface IDb { IEnumerable Get(string query, Action parameterizer, Func selector); int Add(string query, Action parameterizer); int Save(string query, Action parameterizer); int SaveSafely(string query, Action parameterizer); } 

2) Cree la clase auxiliar genérica que no solo debería implementar la interfaz, sino que también debería especificarla con el tipo IDbConnection . La clase debería ser mejor (no necesariamente) instanciable (no estática) para que pueda pasar la cadena de conexión requerida para instanciarla.

Aquí hay una implementación totalmente perezosa:

 using System; using System.Data; using System.Collections.Generic; using System.Linq; public class Db : IDb where T : IDbConnection, new() { string connectionString; public Db(string connectionString) { this.connectionString = connectionString; } IEnumerable Do(string query, Action parameterizer, Func> actor, Func selector) { using (var conn = new T()) { using (var cmd = conn.CreateCommand()) { if (parameterizer != null) parameterizer(cmd); cmd.CommandText = query; cmd.Connection.ConnectionString = connectionString; cmd.Connection.Open(); foreach (var item in actor(cmd)) yield return selector(item); } } } public IEnumerable Get(string query, Action parameterizer, Func selector) { return Do(query, parameterizer, ExecuteReader, selector); } static IEnumerable ExecuteReader(IDbCommand cmd) { using (var r = cmd.ExecuteReader(CommandBehavior.CloseConnection)) while (r.Read()) yield return r; } public int Add(string query, Action parameterizer) { return Do(query, parameterizer, ExecuteReader, r => Convert.ToInt32(r[0])).First(); } public int Save(string query, Action parameterizer) { return Do(query, parameterizer, ExecuteNonQuery, noAffected => noAffected).First(); } static IEnumerable ExecuteNonQuery(IDbCommand cmd) { yield return cmd.ExecuteNonQuery(); } public int SaveSafely(string query, Action parameterizer) { // 'using' clause ensures rollback is called, so no need to explicitly rollback return Do(query, parameterizer, cmd => { using (cmd.Transaction = cmd.Connection.BeginTransaction()) { var noAffected = ExecuteNonQuery(cmd); cmd.Transaction.Commit(); return noAffected; } }, noAffected => noAffected).First(); } } 

Esto solo hace las ExecuteNonQuery básicas ExecuteNonQuery y ExecuteReader , y las Transaction simples. Sin procedimientos almacenados. La función Add funciona para insertar y recuperar la última identificación insertada y “me gusta”. Estaba loco por mí haber hecho las cosas perezosas y haber usado solo una función de ejecución central Do (que se llama para varias acciones de db), y es por eso que Do parece complicado, pero es muy SECO. Idealmente es mejor separarse. También puedes deshacerte de Linq .

3) Por último, proporcione el envoltorio estático Db sin restricciones genéricas en torno a la clase de Db instanciable para que no tenga que seguir pasando el parámetro T cada vez para realizar una consulta de db. Por ejemplo, así:

 public static class Db { static IDb db = GetDbInstance(); static IDb GetDbInstance() { // get these two from config file or somewhere var connectionString = GetConnectionString(); var driver = GetDbType(); // your logic to decide which db is being used // some sort of estimation of your db if (driver == SQLite) return new Db(connectionString); else if (driver == MySQL) return new Db(connectionString); else if (driver == JET) return new Db(connectionString); //etc return null; } public static void Parameterize(this IDbCommand command, string name, object value) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; command.Parameters.Add(parameter); } public static IEnumerable Get(string query, Action parameterizer, Func selector) { return db.Get(query, parameterizer, selector); } public static int Add(string query, Action parameterizer) { return db.Add(query, parameterizer); } public static int Save(string query, Action parameterizer) { return db.Save(query, parameterizer); } public static int SaveSafely(string query, Action parameterizer) { return db.SaveSafely(query, parameterizer); } } 

4) Ahora crearía una función estática adicional GetDbInstance alguna parte para que GetDbInstance los parámetros correctos de la base de datos como cadena de conexión, tipo de proveedor, etc. También tiene un método de extensión para facilitar la parametrización de las consultas. Puse ambos en la clase de Db estática anterior, pero esa es tu elección (algunas personas lo escriben en la clase de Db, pero yo prefiero que esté fuera porque la funcionalidad debería ser tu aplicación).

5) Tenga cuidado de tener consultas neutrales que funcionen en las bases de datos que prefiera.

O

Puede utilizar DbProviderFactory en System.Data.Common para detectar el tipo de DbConnection / provider que tiene. Puede tener solo una clase Db no genérica y hacer:

 public class Db { string connectionString; DbProviderFactory factory; public Db(string driver, string connectionString) { this.factory = DbProviderFactories.GetFactory(driver); this.connectionString = connectionString; } //and your core function would look like IEnumerable Do(string query, Action parameterizer, Func> actor, Func selector) { using (var conn = factory.CreateConnection()) { // and all the remaining code.. } } } 

Su método GetDbInstance se vería así:

 static IDb GetDbInstance() { string connectionString = GetConnectionString(); string driver = GetDriver(); return Db(driver, connectionString); } 

Pro: te deshaces del estilo de progtwigción if-else y se creará una instancia de la versión correcta de la clase Db dependiendo del proveedor y la cadena de conexión en el archivo de configuración.

Con: debe especificar el proveedor / controlador correcto en el archivo de configuración.


Una consulta de muestra de su código C # se vería así:

 string query = "SELECT * FROM User WHERE id=@id AND savedStatus=@savedStatus"; var users = Db.Get(sql, cmd => { cmd.Parameterize("id", 1); cmd.Parameterize("savedStatus", true); }, selector).ToArray(); 

Todo lo que tienes que hacer es llamar a Db.Get , Db.Get , etc. La función GetDbInstance es la clave aquí que encuentra las funciones en los dlls correctos a ser llamados, y la clase auxiliar maneja bien los recursos mientras que, además, hace su tarea de varios operaciones db. Tal clase evitaría la molestia de abrir y cerrar conexiones, liberar recursos, tener que incluir el espacio de nombre dll de la base de datos, etc. todo el tiempo. Esto es lo que se llama DbAL . Puede tener una capa adicional para ayudar a DbAL a comunicarse también entre varias clases de modelo fuertemente tipado. Simplemente amo el poder del polymorphism a través de interfaces y restricciones, ¡lo cual es muy, muy raro! 🙂