Saltar al contenido

Blog técnico | Cambio de bases de datos

En el verano de 2014, sufrió una importante interrupción cuando nuestra base de datos primaria falló. Después de recuperarnos del problema inmediato, decidimos que debíamos migrar a una base de datos completamente diferente para mejorar el rendimiento y la disponibilidad. Aunque llevó algún tiempo completarla, la transición fue sorprendentemente sencilla gracias a un poderoso patrón.

Patrón de depósito

El patrón de repositorio separa las estructuras de datos de la forma en que se persiguen.un repositorio expone una interfaz genérica que oculta cualquier detalle de la base de datos.he aquí un ejemplo artificial de repositorio abstracto en C# para los usuarios:

Blog técnico | Cambio de bases de datos
Blog técnico | Cambio de bases de datos
interfaz públicaRepositorio de usuarios{voidSave(Accountaccount);AccountFind(stringaccountId);AccountFindByEmail(stringemail);List<Account>GetAccountsCreatedBetween(DateTimeOffsetstart,DateTimeOffsetend);List<Account>GetActiveAccounts(intperPage,intoffset);}

El Depósito de Usuarios que implementa esa interfaz se ocupa de todas las consultas a la base de datos y de cualquier mapeo de datos requerido.

El patrón de repositorio es simple de implementar en casi cualquier lenguaje y no requiere la adopción de ningún tipo de marco de trabajo. Puede trabajar con objetos simples de transferencia de datos o clases que encapsulan los datos con el comportamiento.

Dado que nuestro monolito hacía un uso extensivo de este patrón, era una simple cuestión de escribir nuevos repositorios que usaran la misma interfaz pero que accedieran a una base de datos diferente. La inyección de dependencia facilitaba el intercambio de implementaciones, pero también queríamos fomentar la confianza en la nueva base de datos y evitar el tiempo de inactividad para la migración de datos.

¡Decoración al rescate!

Para facilitar el uso de la nueva base de datos, aplicamos el patrón del decorador para crear una tercera instancia de la interfaz del repositorio que componía las otras dos.Inspirado en una entrada del blog de Netflix, el decorador utilizó un conjunto de botones de características para controlar cuándo se utilizaban los repositorios antiguo y nuevo (y, por consiguiente, sus respectivas bases de datos).

Con todos los botones apagados, el decorador sólo actuaría como un paso al antiguo repositorio. Entonces habilitaríamos la escritura dual lo que haría que todas las operaciones de escritura (como Guardar en el ejemplo anterior) fueran a ambas bases de datos. Nos aseguramos de envolver try..catch logic alrededor del nuevo repositorio y registrar cualquier problema. De esta manera podríamos encontrar y arreglar problemas sin afectar a los usuarios.

Con los datos nuevos y actualizados que se escribían en ambas bases de datos, podíamos entonces empezar a migrar los datos existentes de la base de datos antigua.Además de un trabajo estándar de ETL, el decorador también nos proporcionó la opción de escribir programas para simplemente leer y volver a guardar todos los registros.En algunos casos esto era mucho más fácil que tratar de mapear estructuras de tablas disímiles (por ejemplo, documentos NoSQL vs. columnas relacionales SQL).

El siguiente paso fue habilitar la lectura de sombra . Cuando se habilita esta opción, el decorador lee los datos de ambos repositorios, compara los resultados y luego devuelve los datos de la base de datos antigua. Al igual que con la escritura dual, las excepciones de la nueva base de datos se capturaron y registraron para evitar afectar a los usuarios finales. Cuando los resultados diferían, sabíamos que todavía había errores que corregir.sin embargo, algunos métodos de repositorio podían devolver un lote de datos, por lo que para algunos métodos nos saltamos el paso de lectura en sombra y optamos por comprobar la consistencia mediante un proceso separado en lugar de comparar miles (¡o millones!) de registros cada vez.

Como los datos todavía se están escribiendo en ambas bases de datos, podríamos probar la nueva base de datos y volver a la antigua si algo saliera mal.

Una vez que pasó el tiempo suficiente para darnos confianza en la nueva base de datos, dimos el paso final de eliminar el decorador y la antigua interfaz. ¡Migración de la base de datos completada!

Bueno, para un repositorio de todos modos…

Reflexionar sobre la duplicación

Nuestro monolito tenía un buen número de repositorios que necesitaban cambiar. Con pocas excepciones, cada repositorio estaba respaldado por una única tabla de base de datos. Esto mantuvo el acoplamiento bajo, pero significaba que cada repositorio tenía una interfaz diferente y necesitaría su propio decorador. Para evitar la duplicación de la creación de muchos decoradores, algunos de mis inteligentes (¿o quizás atrevidos?) compañeros de trabajo recurrieron al poder de la reflexión.

En primer lugar, crearon algunos atributos para señalar qué métodos de reposición eran escritos y cuáles eran leídos. Por ejemplo:

publicinterfaceIUserRepository{ [RepositoryMethods.Write]voidSave(Accountaccount); [RepositoryMethods.Read]AccountFind(stringaccountId); [RepositoryMethods.Read]AccountFindByEmail(stringemail); [RepositoryMethods. Read]List<Account;GetAccountsCreatedBetween(DateTimeOffsetstart,DateTimeOffsetend); [RepositoryMethods.Read]List<Account;GetActiveAccounts(intperPage,intoffset);}

Luego crearon una clase RepositoryProxy con un método estático para crear el repositorio decorado. Con esos en su lugar, un decorador listo para usar podría ser creado llamando a RepositoryProxy.Create(oldRepo, newRepo);.

Por supuesto, la reflexión puede ser una herramienta peligrosa de manejar. Funcionó bien para nosotros, pero en otros contextos u otros idiomas, puede haber mejores maneras de manejar esto. O puede ser mejor aceptar una pequeña cantidad de repetición en la creación de decoradores.

La base de datos: un detalle de la aplicación

Ser capaz de cambiar de una base de datos a otra es bastante guay. Pero eso es sólo un resultado positivo de un concepto arquitectónico mucho más profundo: tu base de datos debería estar vagamente acoplada a tu código.

He trabajado en bases de datos en las que el código de la base de datos estaba tan entrelazado con la lógica empresarial que había pocas esperanzas de cambiar de una base de datos basada en SQL a otra, por no hablar de pasar a NoSQL.También he trabajado en proyectos en los que un ORM abstrajo nuestra base de datos sólo para introducir un estrecho acoplamiento con ese marco.Esos no eran proyectos divertidos en los que trabajar porque los cambios tendían a ser un trabajo arduo.

Tal vez nunca necesites intercambiar bases de datos. Tal vez un ORM no te dé pena. Tanto si usas el patrón de repositorio como si no, siempre encontrarás beneficios en mantener el acoplamiento de la base de datos bajo. Porque al final del día, la base de datos debería ser sólo un detalle de implementación.

Conclusión

Hay muchas razones por las que se podría cambiar de base de datos, incluyendo factores como la escalabilidad y el costo.en nuestro caso, el monolito estaba usando (o tal vez mal uso ) una base de datos que no nos estaba dando el rendimiento y la disponibilidad que necesitábamos.no nombré las bases de datos específicas aquí para proteger a los inocentes, pero realmente no importa ya que este patrón es agnóstico de base de datos.el uso del patrón de repositorio disminuyó drásticamente la dificultad de cambiar de base de datos.

Detalles de la reflexión

Describir cómo hemos utilizado la reflexión para el RepositoryProxy queda fuera del alcance de este artículo. Los detalles son muy específicos de C#, y honestamente queda fuera de mi zona de confort! Pero para aquellos que estén interesados, aquí hay un ejemplo simplificado:

public classRepositoryProxy<T>:RealProxy{readonlyToldRepository;readonlyTnewRepository;readonlyConfigconfig;RepositoryProxy(ToldRepository,TnewRepository,Configconfig):base(typeof(T)){esto. oldRepository=antiguoRepositorio;this.newRepository=newRepository;this.config=config;}publicstaticTCreate(ToldRepository,TnewRepository,Configconfig){reurn(T)newRepositoryProxy<T>(oldRepository,newRepository,config). GetTransparentProxy();}publicoverrideIMessageInvoke(IMessagemsg){varmethodCall=(IMethodCallMessage)msg;varmethod=(MethodInfo)methodCall.MethodBase;if(method.DeclaringType!=typeof(T)){varreturnValue=method.Invoke(this,methodCall. Args);returnnnewReturnMessage(returnValue,null,0,methodCall.LogicalCallContext,methodCall);}varattributes=method.GetCustomAttributes(typeof(IRepositoryAttribute),true);if(attributes.Any()){varreturnValue=((IRepositoryAttribute)attributes.First()). Execute(methodCall,method,oldRepository,newRepository,config);returnnnewReturnMessage(returnValue,null,0,methodCall.LogicalCallContext,methodCall);}thrownewNotImplementedException($"$0027{method.Name}$0027 no está marcado con un Atributo del Depósito en $0027{typeof (T). Nombre}$0027");}}atributo del repositorio de interfaz pública{objetoEjecutar<T ];(IMethodCallMessagemethodCall,MethodInfomethod,ToldRepository,TnewRepository,Configconfig);}atributo de escritura de clase pública: Attribute,IRepositoryAttribute{publicobjectExecute<T >(IMethodCallMessagemethodCall,MethodInfomethod,ToldRepository,TnewRepository,Configconfig){// lógica para escribir en los repositorios en base a los conmutadores de características configurados}}publicclassReadAttribute: Attribute,IRepositoryAttribute{publicobjectExecute<T >(IMethodCallMessagemethodCall,MethodInfomethod,ToldRepository,TnewRepository,Configconfig){// lógica para leer de los repositorios y comparar los resultados en base a los conmutadores de características configurados}}

Categorías: technicalTags: devops, database, clean code