He leído muchas variantes de cómo lograr lo mismo, todas ellas diferentes y con puntos que me gustan y otros que no. Al final, allá por el año 2007, unos amigos y yo comenzamos a utilizar nuestro propio método el cual ha ido refinándose con el tiempo hasta llegar a lo que es hoy y pienso explicar en este post.
¿Cuál es el problema en cuestión?
Todo aquel que ha desarrollado software en algún momento utilizando un sistema de control de versiones y bases de datos, se ha preguntado cómo lograr incluir esta última conjuntamente con el código. Cada nuevo "commit" que hacemos de nuestro código puede incluir cambios relacionados a actualizaciones en la base de datos, pero dado que esta no se encuentra versionada, las actualizaciones en diferentes entornos pudieran fallar. Lo ideal sería poder versionar la base de datos también, pero desafortunadamente esto no es posible... o al menos no de forma trivial.
Imaginemos que dos desarrolladores se encuentran trabajando conjuntamente en una aplicación determinada. El primero de ellos se encuentra enfrascado en terminar el módulo de administración de los usuarios de la aplicación, por lo que adiciona unos cuantos procedimientos almacenados en su base de datos y una nueva tabla. Cuando todo está terminado, el desarrollador versiona su código pero los cambios en su base de datos se quedan en su copia local, así que cuando el segundo desarrollador actualiza su copia, la aplicación no funcionará correctamente pues el nuevo código hace referencia a tablas y procedimientos almacenados que no existen.
¿Y qué pasa si versionamos siempre una copia de la base de datos? Tampoco funcionará correctamente, ya que en el momento que el segundo desarrollador actualize su copia local, estará sobreescribiendo todos los cambios que él mismo ha hecho. Desafortunadamente, una copia de una base de datos (o "backup" en inglés) es un fichero (o ficheros) binario, por lo que no hay manera de que un sistema de control de versiones pueda detectar cambios en secciones particulares del mismo.
Otra variante bastante utilizada es generar y versionar un script de la base de datos completa, en uno o varios ficheros. Esto obliga a que con cada actualización tengamos que recrear el esquema completo de nuestra base de datos, perdiendo el contenido de todas las tablas. Si queremos actualizar un servidor que contiene información real, no habrá forma de mantener los datos existentes a menos que hagamos unos cuantos malabares.
Y entonces, ¿cuál es la solución?
No estoy seguro que sea la variante perfecta, pero al menos por casi cuatro año me ha funcionado a las mil maravillas. Básicamente el método consiste en crear scripts consecutivos con cada nuevo cambio que hagamos en la base de datos. A la hora de actualizar el código proveniente del sistema de control de versiones, ejecutaremos cada uno de los nuevos scripts en el mismo orden en que fueron creados.
Siguiendo con el ejemplo que veíamos anteriormente, el primer desarrollador versionará cada uno de sus cambios en uno o varios scripts, utilizando una numeración consecutiva para cada nuevo fichero. A la hora de actualizar, el segundo desarrollador verá que se adicionaron nuevos scripts, así que los ejecutará siguiendo el orden correcto y de esta forma su base de datos estará 100% actualizada.
Utilizando este método incluso podemos actualizar un servidor con datos reales siempre y cuando nuestros scripts mantengan el contenido de las tablas. Obviamente, aquí entra en juego la manera en que estos scripts fueron creados, ya que si para adicionar una columna a una tabla se hace un DROP TABLE y luego un CREATE TABLE, los datos desaparecerán.
Prácticas útiles para que todo funcione correctamente
Veamos algunos detalles que debemos tener en cuenta para completar el proceso satisfactoriamente:
- La numeración de los scripts debe ser consecutiva. Personalmente siempre he utilizado la fecha seguida de un número consecutivo. Por ejemplo: 2011020100.sql, 2011020101.sql, 2011020102.sql... O sea, año 2011, mes 02, día 01, y el valor consecutivo comenzando en 00.
- Siempre que sea posible, los scripts deben respetar los datos existentes en la base de datos. Esto es a lo que llamo "safe scripts". Esto no siempre es posible, así que cuando sea el caso, lo indicaremos en el nombre del script de forma tal que antes de ejecutarlo todos sepan que habrán datos que se perderán. Un ejemplo pudiera ser: 2011020105-notsafe.sql.
- Debemos hacer salvas de nuestras bases de datos de forma regular. En el nombre del backup indicaremos el último script que fue ejecutado (o lo que es lo mismo, hasta qué punto esa copia fué actualizada). Por ejemplo: backup-2011020102.bak.
- Antes de versionar los scripts, debemos actualizar nuestra copia local para asegurarnos que no hay nuevos scripts versionados. Tomando el último valor consecutivo, nombraremos nuestros scripts consecuentemente y los versionaremos.
- Nunca, nunca, NUNCA actualizaremos un script previamente versionado. Si ese script ya fué ejecutado en alguna copia de la base de datos, las nuevas modificaciones nunca serán ejecutadas. Si necesitamos hacer algún cambio, siempre crearemos un nuevo script.
- La práctica me ha demostrado que es mucho mejor si cada script contiene poco cambios. Si al ejecutar un script muy grande se produce un error, es bastante engorroso encontrar dónde está el problema.
- Con el paso del tiempo el número de scripts irá creciendo, pero la buena noticia es que una vez ejecutados en todas las bases de datos, los scripts dejan de tener valor, así que podemos agruparlos en ficheros compactados para mantener una copia de los mismos y evitar el cúmulo de ficheros. Agruparlos en paquetes mensuales es lo que siempre he hecho (201011.zip, 201012.zip, etc).
Estoy seguro que no. Las dos desventajas más grandes de este método son, primeramente, que requiere mucha disciplina por parte de todos los desarrolladores para que todo se haga correctamente, y la segunda es precisamente el hecho de que cada uno de los cambios, por muy pequeño que sea, tiene que ser creado en un script. A pesar de esto, es la única variante que me ha funcionado sin problemas durante todo este tiempo, pero como nada es perfecto me gustaría oir la opinión de ustedes al respecto. ¿Hay alguna otra vía más factible? ¿Algo que añadir?
Para agregar un campo no hace falta borrar la tabla, se puede hacer con un alter table, algo como:
ReplyDeleteALTER TABLE `table_name` ADD COLUMN `Column_name` VARCHAR(100) AFTER `field_reference_where_the_new_field_will_be_located`;
SQL server no soporta esto?
Otro, al menos en mí caso uso una tabla como control de versiones dentro de la BD como referencia a que versión esta actualmente, así puedes saber que versión tienes en DEV, UAT, STG o PROD, para esto crea una tabla, que tenga un ID único que sea la versión, en MySQL sería algo así:
CREATE TABLE `tbl_db_schema_version` (
`VERSION_NUMBER` varchar(10) NOT NULL default '0',
`SCRIPT_NAME` varchar(255) NOT NULL,
`APP_RELEASE` varchar(255) NOT NULL,
`COMMENT` varchar(255) default NULL,
`APPLICATION_DATE` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
`APPLIED_BY_USER` varchar(80) NOT NULL,
PRIMARY KEY (`VERSION_NUMBER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Después en cada script de alter que hagas y que nombraras consecutivamente como mencionas, usando preferiblemente la fecha y la versión que la BD que va a tener después de ejecutar el script pones como primera línea algo así:
insert into tbl_db_schema_version (VERSION_NUMBER, SCRIPT_NAME, COMMENT, APP_RELEASE, APPLIED_BY_USER) values (
ReplyDelete'SYS_1.1', 'alter_YYYYMMDD-01.1.sql', 'This is the change that will do something BAD', '0.2', USER());
En la tabla te va a quedar, la versión de la BD, que versión de la aplicación se supone que trabaje con esa BD, que fichero contiene el cambio, quien ejecuto eso y una descripción del cambio que se hizo en la BD.
Como el ID es único si alguien trata de ejecutar de nuevo el script, el mismo fallara por integridad y si usas la herramienta apropiada entonces ninguna de las líneas siguientes se ejecutara. Esto es útil ya que muchas veces la ejecución doble de un script puede destruir los datos.
Si quieres ir un poco mas lejos puedes hacer un modulo en la aplicación que detecte con que versión de la BD puede trabajar la misma, así la aplicación al arranque puede verificar si la BD es soportada y para en caso que no lo sea, esto realmente me hizo falta implementarlo una sola vez por un requerimiento de uno de los clientes.
Mas que eso, si usas herramientas como continuos integration con maven, puedes definir profiles para cada entorno (DEV, UAT, STG and PROD) en dependencia de lo que haya en tu empresa, así corriendo maven puedes crear en un entorno determinado el env de trabajo que sea de forma fácil.
Cada vez que creas un script lo agregas en cada uno de los profiles de maven, así primero lo agregas en DEV, cuando creas que esté listo lo pones en UAT para pasárselo a los QA, después lo pones en STG para las pruebas de aceptación de los usuarios y finalmente feliz lo pones en el profile de producción para cuando ejecutes el script de hacer la promoción. Claro está que esto varía de empresa a empresa, aplicaciones pequeñas quizás trabajen con DEV y PROD y en un caso un poco más feliz, DEV, UAT y PROD, para mi la total realización DEV, UAT, STG y PROD :D
ReplyDeleteEn SQL Server al igual que en Oracle se puede agregar una columna sin tener que droppear(spanglish-Hialeah-FL) una tabla.
ReplyDeleteSyntax:
ALTER TABLE 'tablename' ADD 'columnname' 'type' ['null|not null'] [default value]
i.e. ALTER TABLE MYDB.dbo.tEmployee Add fName varchar(20) not null 'pepe'
To modify existing column type:
i.e. ALTER TABLE MYDB.dbo.tEmployee Alter Column fName varchar(50) NULL
Hola,
ReplyDeleteTe explico como lo hacemos en el proyecto que llevo involucrado durante mas de 3 años.
Contamos con 3 Entornos de BD: Desarrollo, Pre-Producción y Producción. Todo ello contra ORACLE.
Para mi desgracia el versionado de documentación y fuentes se basa en VSS, pero eso es un tema que dejo de lado...
Somos 2 en el equipo actualmente aunque en momentos puntuales hemos llegado a ser 4.
La coordinación de código fuente es "evidente" con VSS.
Para el tema de las BD, las diseñamos a través de Erwin Data Modeler, por lo que podemos generar los scripts diferenciales ed la BD.
Lo más normal y habitual, al menos hasta ahora, es que se engloben un conjunto de cambios en la BD que van a ir integramente ligados a una instalación de código fuente. Para ello con Erwin, podemos generar un script con las diferencias existentes con el servidor indicado y el esquema de la BD que estamos diseñando.
Con ello hacemos algo parecido a lo que tu indicas, tenemos una serie de instalables y si el instalable conlleva modificaciones de BD, lleva consigo un Script con las instrucciones SQL necesarias. En nuestro caso concreto, no es necesario que ejecutemos todos los scripts para recuperar la BD, con ejecutar el diferencial vale, y en el caso de que tuvieramos que generar toda la BD desde 0 lo hariamos directamente desde Erwin.
En cuanto a lo que tu comentas, en el caso de que no lo hicieramos asi y que cada desarrollador hiciera la "guerra" por su lado, sería muy importante lo que comentas en uno de los comentarios, las columnas tienen que admitir nulos, al menos hasta que el resto de desarrolladores tengan la ultima versión del código fuente, a partir de ese momento si que se podría llevar a cabo quitar la admisión de nulos y tal...
Perdón por la parrafada!!
En mi caso, con una aplicación instalada en más de 40 clientes, necesito que la actualización sea automática: que cada nuevo lanzamiento del programa sea capaz de actualizarla.
ReplyDeletePara esto, en la base de datos cuento con un número de versión guardado en una tabla de parámetros globales. En la aplicación hay un archivo de recursos donde voy añadiendo numerados ('Act00001') los nuevos scripts necesarios. Al ejecutar la aplicación, comprueba la versión de la base de datos y la actualiza si hay nuevos scripts que pasar.
Es muy recomendable que este sea el único camino de actualización, incluso de las bases de datos de desarrollo, por lo que sólo uso el administrador de SQL Server para corregir o deshacer actualizaciones antes de repetirlas.
En realidad es un poco más complejo, porque mi aplicación se compone de varias librerías de negocio, y cada una tiene su propia versión, pero es lo mismo de antes repetido: la base de datos tiene un número de versión para cada librería, y cada una se comprueba por orden y actualiza si procede.
Como veis, tiene puntos en común con vuestras soluciones, pero quería destacar el aspecto de que todo el proceso va incluido en el ejecutable.
Y para terminar: ¿Para cuándo un soporte para esto en Entity Framework? ¿Sabéis si hay algo así en NHibernate? He oído que Ruby-on-Rails sí tiene un buen soporte para esto, ¿lo habéis probado?
Como han dicho anteriormente el sistema que utilizamos es ese, básicamente, incluso con una tabla que indica la versión y la fecha, para saber cual tiene cada versión.
ReplyDeleteLos desarrolladores utilizan el software de RedGate para crear los scripts diferenciales, después de actualizar a la última versión claro.
Y luego las actualizaciones del programa comparan la versión y se descargan desde nuestro servidor web, los script de actualización hasta llegar a la necesaria para esa actualización, con eso conseguimos que si se saltan alguna actualización de software, lo cual no es importante, la BD si pase por todos los script necesarios para "sincronizarse" con la que necesita la versión del software.
Ningún script puede/debe eliminar datos, para crear/eliminar campos existe ALTER TABLE y creo que lo demás que hacemos es una refusión de los comentarios anteriores.
Por supuesto que para adicionar un campo no hay que borrar la tabla. Lo soporta SQL Server, Oracle, MySql y estoy seguro que cualquier sistema gestor que se respete. Lo que quería decir en el post es que en los scripts NO se puede hacer un DROP, sino un ALTER.
ReplyDeleteEl otro punto interesante que han mencionado en los comentarios es el hecho de mantener en la misma DB una tabla con la información de los scripts. Una vez lo hice y es bastante práctico. Junto con esta información también almacenabamos el número de la revisión del SVN para saber exactamente esa BD con qué versión del código corría bien.