sábado, 26 de noviembre de 2011

Rails legacy

¿Que tipos de legacy tenemos?
  1. El código que hice hace seis meses, cuando mis conocimientos eran inferiores a los actuales [fácil]
  2. El código que no he echo yo, sino un compañero, otro equipo o una empresa de terceros (en este orden) [moderado]
  3. El código o aplicación que ha estado o está funcionando desde hace "tiempo" con una tecnología distinta a rails, distinguiendo:
    1. Aplicación a recaer [moderado]
    2. Aplicación a heredar (los datos, el funcionamiento) o convivir [difícil]
Podemos entrar en muchos matices y seguramente definir una lista más completa, en todo caso para los dos primeros puntos, dos conejos rápidos:
  1. Si no tengo test añadirlos, si tengo entenderlos, añadir más tests... he dicho test? ¡Pues eso, test, test, test!
  2. Refactoring a demanda, cuando se tenga que modificar algo se aprovecha y se corrige.
Para el tercer punto, sigue leyendo ya que es el motivo principal de este articulo.

¿Rails es legacy?

Si!

¿Rails está pensado para legacy?

En mi opinión, no! La filosofía core de rails es "Convention over Configuration (CoC)". Una aplicación legacy raramente seguirá el convenio de rails, con lo que a priori ya perdemos o tenemos que añadir el coste de ir especificando todo lo que no siga el convenio, nombres de tablas, tipos, joins, etc... Lo segundo y derivado de este problema, es que al salirnos del core de rails y ampliar con gems, nos encontraremos que algunas de ellas están solo pensadas para aplicaciones que sigan el convenio y no tienen toda la especificación rails implementada. Si no tienes alternativa sigue leyendo.


Primera solución: mapeando con vistas

Como: creando vistas que renombren la tabla, las columna y los tipos al convenio de rails y luego creando modelos rails que nos permitan acceder a ellos.

Limitaciones: solo lectura (* dependiendo del SGBD).

Problemas habituales y posibles soluciones / alternativas: 
  • Indice compuesto por strings:
    1. Mantenemos el string como indice (nos saltamos el convenio)
    2. Creamos una tabla intermedia que traduzca de strings a integer y ligamos su mantenimiento con trigers.
    3. Creamos una función que traduzca de strings a integers (afecta al rendimiento y puedes tener claves muy largas)
  • Indice compuesto por múltiples columnas: juntamos las columans en la vista

Segunda solución: mapeado en modelos las cuatro coas más importantes

En el modelo:

nombre de la tabla personalizado:
        set_table_name "client"

nombre del identificador principal o clave primaria (sin clave compuesta)
set_primary_key "client_id"
* Tener en cuenta que la opción "find_by_id" deja de funcionar, podemos utilizar "find_by"o "find_by_client_id"
 nombre del identificador principal o clave primaria (con clave compuesta)
Instal·lamos la gem composite_primary_keys
Especificamos: "set_primary_keys :person_id, :group_id"
 renombrado columnas puntuales
alias_attibute "person_name_full", "name"
* Aquí los find funcionan para el alias.
definiendo relaciones con otros modelos
has_many :comments,
 :class_name => "WordpressComment"
 :foreign_key => "comment_post_ID"
* Ejemplo real: tabla "post" con la columna "comment_post_ID" que mapeamos contra la tabla del modelo "WordpressComment" con el nombre de tabla y el id que tenga definidos.
definiendo relaciones con otros modelos y claves compuestas:
Si la cosa se complica mucho, mi recomendación es utilizar métodos dentro del modelo que nos permitan realizar las búsquedas:
def comments
    WordpressComment.find_by_post_id(self.coment_post_id)
end
 Forzar los plurales "config/initializers/inflections.rb":

* Partiendo del nombre de la tabla y añadiendo una "s"
inflect.irregular 'imp_person', 'imp_persons'
inflect.irregular 'imp_comments', 'imp_commentss'
 En las migrations:

Siempre que quieras crearlas (lo normal es que venga hechas).

si el campo id no existe, especificarlo
def change
   create_table :imp_person, {:id => false} do |t|

...
si no hay fechas de control (eliminarlas)

t.timestamps
si tenemos clave primaria añadirle el indice con execute
def up
  execute "ALTER TABLE person ADD PRIMARY KEY (national_document);"
end
def down
  execute "ALTER TABLE person DROP PRIMARY KEY (national_document);"
end

Un par de recomendacoines más:
  1. Valorar el añadir un prefijo a los modelos, de esta forma juntamos toda la lógica.
  2. Dado el coste de definir, lo definimos todo, aún siguiendo el convenio de rails, té evitarás problemas raros.
  3. Si has generado el modelo con scaffold reviste las fixtures, es posible que no te sirvan (y escribe test!).

Otro escenario habitual en legacy: nueva aplicación que tiene que heredar los datos del sistema anterior. Aquí es habitual encontrarse con los mismos problemas de convenio, más las limitaciones derivadas de los nuevos modelos de datos, campos que se añaden, campos que se quitan, nuevas reglas de negocio que pueden provocar que nuestros datos no se validen, nuevas columnas que no podemos rellenar etc... La solución dependerá mucho de nuestro escenario, en general suele dar muy buen resultado crear modelos paralelos más o menos ligados con el principal, por ejemplo podemos tener el modelo "Client" y "ClientLegacy", aquí ya es la habilidad de cada uno para reducir código, solo una nota, en mi opinión es mejor mantener los dos modelos con dos tablas distintas (para mi la validación en BD es igual de importante que en el modelo y claramente ambos tendrán validaciones distintas).



Hasta aquí esta primera aproximación al mundo legacy, en futuros artículos más escenarios, soluciones y experiencias.

sábado, 19 de noviembre de 2011

¿Como evitar problemas en las migrations?

Primero de todo teniendo en cuenta para que sirven:
  1. Actualizar el esquema de base de datos (añadir tablas, campos, índices, eliminar...) [50% del valor total]
  2. Automatizar transformaciones o scripts para mover datos en el despliegue de producción (típico caso de una tabla que se divide en dos y tenemos que recolocar los datos con cierta lógica). [30% del valor total]
  3. Poder movernos en la línea temporal y en caso de necesidad poder bajar a una versión de código inferior con una base de datos acorde. [20% del valor total]
Estos tres puntos son lo más importante y lo que más dolores de cabeza trae en entornos donde no hay una solución equivalente a las migrations (que muchas veces termina siendo un fichero leame o muchos scripts que la gente tiene que ir haciendo y que la persona que despliega tiene que acordarse de ejecutar en en desarrollo, producción, test, ...) todo lo demás son añadidos o como dirían los yankis "sugar", y me refiero a todo, des de los métodos del "Active Record" hasta el potencial derivado de utilizar código rails. Y el azúcar en exceso es malo (cita de mi médico).

Cuando aparecen los problemas:
  1. Al intentar hacer las migrations des de zero [ahora] hay algún paso que falla.
  2. Al desplegar en el entorno de producción/test falla.
Las causas de los problemas:

Modificación a posteriori de una migration subida al repositorio (GIT): típico caso donde alguien se ha dejado de añadir un campo, el nombre estaba mal escrito, el tipo de datos no era el necesario y en lugar de hacer una nueva migration se decide sobrescribir la original y subirlo al repositorio GIT. En que escenarios falla:
  • Si en lapos de tiempo entre la primera y la segunda versión, alguien había bajado los cambios del repositorio y había ejecutado "rake db:migrate". Aún bajando la nueva versión que podamos subir esa migration no volverá a ejectuarse y nuestro compañero quedará tocado.
  • Si tenemos sistemas automatizados como el servidor de integración continua, van a sufrir los mismos problemas. (Siempre y cuando este servidor rake db:migrate)
  • Si queremos movernos en la línea temporal de nuestro código o entre versiones (commits) es posible que nos encontremos con inconsistencias.
  • Favorece la aparición de condicionales, ejemplo añade una columna si no existe....
Esté caso se da con mucha frecuencia cuando tenemos un programador que percibe las migrations como parte del código a mantener "limpio" y ordenado, en lugar de ser una secuencia cronológica de cambios y mejoras, tener muchas migrations no es malo.

Haber implementado solo "up" si solo nos preocupamos de migrar para adelante estamos perdiendo el 20% del potencial y nos exponemos a:
  • En caso de error en producción si tenemos que retroceder de versión nos quedaremos con un código inconsistente ya que la base de datos no habrá bajado de versión (a no ser que recuperemos la copia de seguridad). Esto puede suceder tanto en el momento del deploy como al cabo de unos días que se decida volver a la versión antigua.
  • No poder probar el código entre versiones, solo podremos trabajar en la última versión y si tenemos que hacer un "rake db:migrate:redo" tendremos que ayudarnos de sql a mano.
  • Favorecer los condicionales en las migrations, mismo caso, añade la columna si no existe (porqué antes no la hemos borrado).
Por suerte el generador de scaffold ya nos ayuda cuando somo novatos y en rails 3.1 ha aparecido la opción "change" que tiene un número limitado de métodos (add_column, add_index, add_timestamp, create_table, remove_timestamps, rename_column, rename_index, rename_table) que no hace falta escribir "up" y "down"  y el solo sabe resolver. Para todo lo demás valorar

Pasar del "active record" y utilizar execute, a no ser que tengamos muy claro y pactado con el equipo y el cliente que vamos a utilizar un SGBD concreto, añadir "execute" nos limita a la hora de cambiar de base de datos, nos obliga a aprender y entender SQL (eso no quita que como programador debas conocerlo) y en general favorece el espagueti coding (sentencias de más de una linea con mega construcciones de tildes)

Utilizar modelos en nuestras migrations, es hambre para hoy y pan para mañana, a la larga cuando añadamos complejidad a nuestros modelos terminará generando problemas, el ejemplo:
Tenemos una migration que crea una tabla de usuarios con el campo correo (como login) y contraseña y utilizamos la misma migration para poner el usuario admin y de esta forma asegurar que siempre tenemos un primer usuario, más adelante decidimos ampliar la tabla con el campo nombre de usuario y lo hacemos obligatorio, des de este momento no podremos volver a hacer el up de esta migratio, ya que nos fallará al no tener informado el nombre de usuario.
Situaciones que lo promueven:
  • Populamos la base de datos des de las migrations. Es un error, debemos popular des del seed (prometo un articulo dedicado al seed y como popular).
  • Hacemos cambios de estructura (el ejemplo de pasar de una tabla a dos y tener que repartir la información). Ya de por si es un caso complejo, porqué nos encontramos con el código y los modelos en una versión superior, seguramente estaremos utilizando execute para acceder a la información antigua, con lo que mi recomendación es tratarlo como un caso excepcional y hacerlo todo con execute y sql directo, intentando ser lo más curiosos y encapsulando todo lo que podamos.


En caso de pánico cargar el fichero schema.rb con "db:schema:load" or "rake db:setup", el segundo incluye la carga del seed. Warning: ambas ordenes recrean la base de datos de zero, con lo que perderemos todos los registros, con lo que esto nos puede servir para un desarrollador que se une al equipo o un entorno de test / preproducción.

El resumen de consejos es:
  • Si se ha versionado (en git commit) no se puede modificar. Cremos una nueva migration y corregimos. [riesgo alto]
  • Siempre implementar la opción de up y down conjuntamente. [riesgo moderado]
  • Utilizar "active record" y evitar execute siempre que se pueda. [limitación al cambiar de SGBD]
  • Utilizar modelos en nuestras migrations. [riesgo alto]
  • Tener muchas migrations no es malo. [desmintiendo el mito]

domingo, 13 de noviembre de 2011

Git workflow

Ligado con el proyecto de infraestructura es necesario tener un plan de trabajo que nos permita trabajar de forma cómoda a la vez que resolver nuestros problemas con la mayor brevedad y menor incidencia.

En un ciclo normal de desarrollo, el equipo nos reunimos con el cliente, revisamos el estado del proyecto y definimos una serie de historias de usuario / funcionalidades / necesidades que creemos podemos desarrollar en un tiempo corto (idealmente para la siguiente itreación), una vez terminadas o alcanzado el plazo pactado, pasamos nuestras funcionalidades nuevas a release (no tiene porqué ser al final, puede ser progresivo), las validamos con el cliente y si todo va bien las ponemos en producción.
¿Que sucede si no se aprueba la release? Tenemos dos escenarios:

Idealmente tendríamos que corregirlo en develop y luego volver a actualizar release, con lo que si algun otro programador está trabajando en develop, podremos no solo corregir el problema sino también añadir nuevo valor al cliente (siguiendo el flujo de mejora continuada).

Si por alguna razón develop no está estable (se ha perdido funcionalidad, se ha dejado algo que antes funcionaba a medias, etc, ...) tenemos un problema serio. No obstante podemos hacer una copia de la branca release, corregir el problema, hacer merge en release, validarlo con el usuario y hacer merge en develop.
Como vemos a la práctica es más costoso, requiere de más merges, tenemos cosas nuevas que el usuario final aun no puede ver.

¿Que sucede si aparece un error en producción?

Primero de todo analizar el nivel de criticidad, es realmente un error, cumple nuestro requisito de hotfix, si es que sí hacemos lo siguiente:
  1. Copia de master en local 
  2. Realizamos un test que reproduzca el problema
  3. Corregimos, merge a master, validamos con el usuario.
  4. Merge a develop


El segundo merge lo hacemos en develop, para tener nuestra branca "principal" de desarrollo actualizada, no obstante tenemos un escenario donde podemos valorarlo:
  • Tenemos entorno de pre-producción y no podemos / queremos que el error se pueda reproduzir allí.
Típicamente en este punto queda la duda si se puede perder un hotfix con algún merge, por ejemplo con uno de release (sin hotfix) a master (con hotfix), en este caso si el merge funciona bien (que es lo normal) se mantiene el hotfix en master.

Un pequeño resumen de las brancas que tenemos:

Origin / Master (M):
  • Branca estable (en producción)
  • Solo HOTFIX (error grave, requerimiento legal, ..), con copia a develop.
Origin / Release (R):
  • Futura versión a desplegar
  • Funcionalidades completas y terminadas
  • Solo correcciones menores que no se pueden hacer en develop, con copia a develop.
Origin / Develop (D):
  • Las nuevas funcionalidades van aquí
  • Branca compartida entre programadores
  • Si no está aquí no existe (no se mantiene, prueba, etc..)
  • No hay funcionalidades a medias, subimos cuando está listo para validar (por otros programadores, el cliente, …) [idealmente]

Un árbol resumen de que atacar y en que orden:

Y como siempre el resumen final (click para ampliar):

Agradecer el articulo de Vincent Driessen en http://nvie.com/posts/a-successful-git-branching-model/ que en su día nos ayudo a tener una idea clara de como trabajar con GIT.

sábado, 5 de noviembre de 2011

ActiveRecord CallBacks

Cuando los modelos se empiezan a complicar es bastante útil conocer los distintos métodos que tenemos para interferir en los procesos de transferencia entre la base de datos y nuestros modelos, aquí viene un pequeño resumen:


Cargando objetos:

Nos permiten alterar los datos o intervenir en el proceso de carga en memoria.
  • after_initialize: cuando queremos interferir al crear / cargar (instanciar) un objeto.
  • after_find: cuando se carga en memoria un objeto de la base de datos (model.find, model.first..). 
Tener en cuenta que primero se lanza "after_find" y luego "after_initialize".

def after_initialize
    user.name = self.name.capitalize
end

user = User.new(:name => 'ADMIN') 
puts user.name 
=> Admin

Si queremos que algo solo se aplique a los modelos que provienen de la base de datos, debemos utilizar el "after_find" (para marcar que es un modelo con A.R.) y si queremos que se aplique a todo (creados en memoria y leídos de la base de datos), tenemos que utilizar el "after_initialize".


Guardando objetos (nuevos o existentes):

Están disponible al invocar save (user.save)
  • before_validation i [before_validation_on_create | before_validation_on_update]: típicamente para automatizar campos (calculados, assegurar-se que un valor no es nulo...).
  • after_validation, [after_validation_on_create | after_validation_on_update], before_save: típicamente para validar la lógica del modelo y de la aplicación en conjunto.
  • [arround_insert | arround_update]: guardado físico y validación por parte de la base de datos.
  • [after_create | after_update] y after_save: desencadenadores que nos permiten actualizar contadonres, actualitzar dependencias, comunicar acciones o eventos por correo o servicios web, etz..


Eliminando objetos (existentes en la base de datos):

Están disponibles al invocar destroy (user.destroy)
  • before_destroy: validar la lógica del modelo y de la aplicación en conjunto, por ejemplo dependencias.
  • arround_destroy: guardado físico y validación por parte de la base de datos.
  • after_destroy: desencadenadores que nos permiten actualizar contadonres, actualitzar dependencias, comunicar acciones o eventos por correo o servicios web, etz..


Guardado conjunto con transacciones:

Si tenemos una transaccion definida (ejemplo típico de transferir dinero de una cuenta a otra)

SendMoney.transaction do
    customer_1.save
    customer_2.save
end

Tenemos disponible:
  • after_commit: cuando la transacción y todos sus pasos se han realizado correctamente
  • after_rollback: cuando la transacción a fallado, porqué uno de los pasos a fallado.


Guardado con relaciones o anidado (relations and nested atributes):

Si tenemos modelos con relaciones definidas (has_many, belongs_to, ...) podemos aprovechar los métodos siguiente métodos para interferir en su creación:

  • before_add y after_add: al crear
  • before_remove y after_remove: al eliminar

class Customer < ActiveRecord::Base
    has_many :orders, :before_add => :check_credit_limit
    ...


Cuadro resumen:



Más información en:


martes, 25 de octubre de 2011

Creando PDF's con PRAWN

Si quieres crear informes o documents PDF's a demanda (dinamicamente) y aún no has escogido ninguna solución, no dejes de echarle un vistazo a PRAWN.

Simples pasos para la instalación:
  1. Editar el fichero "gemfile" y añadir "gem 'prawn' " 
  2. Linea de comandos y "bundle install"
  3. En "app" creamos una carpeta "app/reports" donde guardaremos nuestros generadores de PDF's
  4. En "app/reports" creamos una caperta "app/reports/images" donde guardaremos las imágenes que añadamos a nuestros PDF's
  5. Editamos nuestro fichero "application.rb" y añadimos la carpeta que hemos creado.
    config.autoload_paths << "#(Rails.root)/app/reports" #PDF custom generators
  6. Bajarnos el manual de http://prawn.majesticseacreature.com/manual.pdf y ponerlo en "doc/prawn_manual"
¿Cómo creamos nuestro primer report? 
  1. Creamos nuestro generador PDF en "app/reports/products_report1.rb" y "app/reports/products_report2.rb"
  2. Lo rellenamos con algún ejemplo dumy
    Ejemplo1:

    Ejemplo2:
  3. Creamos el punto de entrada en el controlador "app/controller/products_controller.rb"
  4. Añadimos la ruta en el "config/routes.rb"
  5. Añadimos un botón o link al informe en "app/views/products/index.html.erb"
    <%=link_to 'report1',  print1_products_url%> -
    <%=link_to 'report2',  print2_products_url%> <br />
Des de aquí es solo ampliar los generadores que hemos creado, para que mediante el controlador vaya renderizando la información que nos interese.

Para revisar el proyecto de ejemplo en profundidad puedes ir a https://github.com/micues/prawn_test

También te será útil revisar-te el manual que hemos adjuntado en la documentación para ver más en detalle que objectos y funciones puedes utilizar con prawn.

    viernes, 21 de octubre de 2011

    Ubuntu RoR + postgresSQL 9 -> sobre windows

    Si tu entorno "natural" es windows, pero tu desarrollo es rails, hay algunas situaciones donde te puede interesar tener una ubuntu con rails funcionando:
    • Para probar el sistema en un entorno parecido al de producción (en producción windows + rails está prohibido).
    • Para las gem que no se pueden compilar con el DevKit o no tienen versión "-x86-mingw32"(pre-compilada).
    • Para aumentar la velocidad en las pruebas en el servidor de desarrollo (sobretodo arranque)
    • Para aumentar la velocidad en los test (esta es mi principal)
    Para conseguirlo unos simples pasos:
    1. Descargamos virtual box (porqué es gratuito multi plataforma)
      https://www.virtualbox.org/wiki/Downloads
      I escogemos VirtualBox 4.1.x for Windows hosts x86/amd64
    2. Instalación dummy de virtual box (siguiente-siguiente)
    3. Descargamos la última versión de ubuntu
      1. Vamos a http://www.ubuntu.com/download/server/download
      2. Seleccionamos la versión a descargar. Mi recomendación la última LTS (Long-time-suport) que seguramente será la que tengamos en el servidor.
        Las versiones LTS (Long Term Support), que se liberan cada dos años, reciben soporte durante tres años en los sistemas de escritorio y cinco para la edición orientada a servidores
        Fuente: http://es.wikipedia.org/wiki/Ubuntu
      3. Guardamos la iso en un lugar conocido.
    4. Creamos la máquina virtual. Inicio -> Ejecutar-> "Oracle VM VirtualBox" y le damos a "New" o CTRL+N. A partir de aquí es un wizzard, nombre, versión... ram, disco, que tener en cuenta:
      1. Sistema operativo igual al que vamos a instalar Linux y ubuntu amb64 o ubunty 32 bits i386
      2. Disco tipo VDI (virtual box disk image) y "fixed size" en lugar de "dynamically allocated", para mejorar la velocidad.[recomendación]



        Si por equivocación le damos a cancelar al wizzard siempre podemos añadir la imagen des de "Devices" -> "CD/DVD's"

    5. Instalar el sistema operativo. Seleccionamos la máquina instalada y le damos al botón "Start" o "Run" y la primera vez no salé un wizzard. En la segunda pantalla del wizzard podemos escoger montar una unidad con la imagen que hemos bajado y de esta forma nos arranca la máquina con el wizzard de instalación de ubuntu.



    6. Instalación dummy de ubuntu (siguiente-siguiente)



      Del wizzard solo hace falta cambiar el teclado al que tengamos y escoger un nombre de usuario y contraseña.

    7. Para instalar rails (tiene que ser en una instalación limpia), linea de comandos y
      wget --no-check-certificate https://raw.github.com/joshfng/railsready/master/railsready.sh && bash railsready.sh
      + información en https://github.com/joshfng/railsready

      El script nos pregunta si queremos RVM (Ruby Version Manager) que nos permitiría en un futuro instalar en la misma máquina otra version de ruby que no sea la 1.9.2. Es muy recomendable trabajar en RVM pero si no lo ves claro (o quieres la máquina lo más simple posible) siempre puedes crear otra máquina virtual y instalar la versión de ruby superior cuando la necesites.

      Para que funcione necesitamos tener internet accesible des de la máquina virtual.

    8. Para instalar postgresSQL, linea de comandos y
      1. sudo apt-get install python-software-properties
      2. sudo add-apt-repository ppa:pitti/postgresql
      3. sudo apt-get update
      4. sudo apt-get install postgresql

      5. ¿Por qué? Porqué ubutnu no puede dar soporte a todas la versiones de PostgresSQL y la oficial es la 8.2 (a noviembre 2011), en futuras versiones se irá actualizando. Entonces para evitar tener que bajarnos la versión de la página oficial http://www.postgresql.org/download/ y compilarla por nosotros mismos, lo que hacemos es añadir un segundo respositorio de software (market / app store) y bajarnos la versión des de allí. 
        (http://www.dctrwatson.com/2010/09/installing-postgresql-9-0-on-ubuntu-10-04/)

    9. Des de aquí la instalacón es igual que en windows 
      1. linea de comandos y "gem install rails"

    En este punto deberías tener una máquina preparada para desarrollar, recuerda de realizar la performance / tuning básica al postgres (de serie viene preparada para un 386) y instalar GIT.

    Una buena solución que se integra muy bien con Windows es utilizar las "shared folders" de la virtual box, las encontrarás en "Devices" -> "Shared Folder" y te permiten mapear tus carpetas de windwos en ubuntu.

    Una última recomendación para VirtualBox es cambiar en "Devices" -> "Network Adapters" la conexión de "Nat" a "Bridge" (para que la máquina sea visible des de fuera) y instalar la "Guest additions" que encontramos en "Devices".