Corriendo aplicaciones Rails en JBoss

El objetivo de esta anotación es mostrar los pasos necesarios para lanzar una aplicación Rails dentro de un servidor de aplicaciones Java, en este caso el popular JBoss La intención última es comprobar los progresos está llevando a cabo el equipo de JRuby. Quién nos lo iba a decir hace un año, así que… ¿quién nos dirá qué va a ocurrir para el año que viene?

Instalación de JRuby

El primer paso será la instalación de JRuby , por supuesto. Damos por ya instalado Java en nuestra máquina. Para Mac OS X, la instalación por defecto de Java será suficiente (lo cual es raro)

1
2
3
  cd ~/jrubydemo/
  wget http://dist.codehaus.org/jruby/jruby-bin-1.0.1.tar.gz
  tar zxvf jruby-bin-1.0.1.tar.gz

Y se creará el directorio jruby-1.0.1. A continuación, añadiremos las siguientes variables al entorno de ejecución actual, una forma de hacerlo es la siguiente

1
2
3
Tamarindo:~/jrubydemo/ juan$ cat > entorno
export JRUBY_HOME=`pwd`/jruby-1.0.1
export PATH=$JRUBY_HOME/bin:$PATH

Cada vez que cambiemos este fichero tendremos que acordarnos de ejecutar el nuevo entorno en todas las sesiones que tengamos abiertas, hasta que añadamos estas variables al entorno por defecto.


. entorno

Ya podemos comprobar que las variables de entorno están presentes. Posteriormente podremos añadir la carga de este entorno a nuestro profile, pero de momento nos bastará con recordar que hay que evaluar este archivo de entorno para poder trabajar con JRuby. Ya podemos hacer nuestra primera, emocionante prueba:

1
2
Tamarindo:~ juan$ jruby -v
ruby 1.8.5 (2007-08-23 rev 4201) [i386-jruby1.0.1]
1
2
Tamarindo:~ juan$ which gem
/Users/juan/jrubydemo/jruby-1.0.1/bin/gem

JRuby incluye su propia versión de gem, y todas las gemas que instalemos con el entorno de JRuby quedarán instaladas en $JRUBY_HOME/lib/gems, de manera que no romperemos nuestra instalación estándar de Rails y otra gemas.

Instalación de Rails sobre JRuby

Merced a la flexibilidad del sistema de paquetes de Ruby, este paso será muy sencillo. Para instalar a los sospechosos habituales bastará con:

1
2
gem install rails -y --no-rdoc
gem install activerecord-jdbc --no-rdoc --no-ri

Esto instalará las gemas habituales: ActionMailer, ActionPack, ActiveSupport, ActiveRecord, Rails, Rake, RSpec, etc. asi como el adaptador JDBC para ActiveRecord. En este punto es cuando cualquier tutorial de Rails que se precie creará una aplicación de prueba (por ejemplo, un blog, un CMS, un mashup o cualquier otra cosa) en menos de 5 minutos. Nosotros, que somos algo más vagos, optaremos por instalar una aplicación que ya tenemos lista en su repositorio de Subversion:

1
2
3
4
cd ~/jbossdemo
svn checkout  http://mi-repositorio.com/myapp/trunk myapp
cd myapp
jruby script/server

Y, si la base de datos está arrancada y ’’config/database.yml’’ está bien configurado, la aplicación funcionará sin más dilación. El mismo código de nuestra aplicación funcionará tanto sobre la implementación de Ruby en C que sobre la implementación en Java. No es poco.

Más difícil todavía: JBoss

Descargaremos JBoss de Sourceforge. El paquete zip es algo más pequeño:

1
2
3
4
http://labs.jboss.com/jbossas/downloads/   (4.2.1GA)
cd ~/jrubydemo/
wget http://downloads.sourceforge.net/jboss/jboss-4.2.1.GA.zip?modtime=1184616808&big_mirror=1
unzip jboss-4.2.1.GA.zip  (la versión zip es más pequeña)

Y por supuesto a nuestro fichero de entorno añadiremos:

1
2
3
4
cd ~/jbossdemo
export JRUBY_HOME=`pwd`/jruby-1.0.1
export JBOSS_HOME=`pwd`/jboss-4.2.1.GA
export PATH=$JRUBY_HOME/bin:$PATH

Configurando el acceso a bases de datos en JBoss

Este paso es lógicamente específico de JBoss, pero es necesario para correr nuestra aplicación. Dado que en el mundo Java no está aún muy extendido lo de la convención sobre configuración, tendremos que explayarnos un poco en el acceso a la base de datos, creando el fichero ’’jboss-4.2.1.GA/server/default/deploy/mysql-ds.xml’‘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<datasources>
  <local-tx-datasource>
    <jndi-name>MySqlDS</jndi-name>
    <connection-url>jdbc:mysql://mysql-hostname:3306/myapp</connection-url>
    <driver-class>com.mysql.jdbc.Driver</driver-class>
    <user-name>usuario</user-name>
    <password>clave</password>
    <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>

    <metadata>
       <type-mapping>mySQL</type-mapping>
    </metadata>
  </local-tx-datasource>
</datasources>

Con esta configuración le hemos dicho a JBoss que acceda a MySQL usando JDBC para acceder a la base de datos. Pero aún necesitamos instalar el correspondiente driver JDBC para Java, lo que MySQL denomina conector . De esta página podremos descargar el conector en forma de archivo .zip y la instalación es tan sencilla como copiar un archivo ’’.jar’’ al directorio lib del directorio de nuestro servidor.

1
2
3
4
cd ~/jbossdemo
wget http://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.3-rc.zip/from/http://mysql.rediris.es/
unzip mysql-connector-java-5.1.3-rc.zip
cp mysql-connector-java-5.1.3-rc/mysql-connector-java-5.1.3-rc-bin.jar  jboss-4.2.1.GA/server/default/lib/

Con todo esto, ya podremos probar si JBoss goza de buena salud.

cd $JBOSS_HOME
bin/run.sh

Tras varios mensajes esotéricos, JBoss cobra vida:

INFO  [Server] JBoss (MX MicroKernel) [4.2.1.GA (build: SVNTag=JBoss_4_2_1_GA date=200707131605)] Started in 16s:863ms

También nos interesa el mensaje

[ConnectionFactoryBindingService] Bound ConnectionManager 'jboss.jca:service=DataSourceBinding,name=MySqlDS' to JNDI name 'java:MySqlDS'

que indica que JBoss ha creado el gestor de conexiones MySQL. Es hora de mudar nuestra aplicación Rails.

Creando un archivo .war

La manera estándar de desplegar aplicaciones dentro de un contenedor Java es mediante el uso de archivos war” los cuales no son más que ficheros zip que contienen toda una jerarquía de directorios con clases y recursos estáticos (HTML, imágenes, etc) aderezados con ciertos metadatos. Cuando uno de estos ficheros aparece en el directorio adecuado del servidor, éste lo abre, analiza los metadatos y toma las acciones pertinentes para activar la aplicación.

Así, nos hace falta crear un fichero war que contenga nuestra aplicación Rails. Afortunadamente, no tenemos que preocuparnos mucho del asunto porque ya existe un plugin Rails que se encarga de ello, creado por Charles Nutter. Tenemos toda la información necesaria en este wiki

cd ~/jbossdemo/myapp
jruby script/plugin install svn://rubyforge.org/var/svn/jruby-extras/trunk/rails-integration/plugins/goldspike

Para configurar los contenidos de nuestro war no todo van a ser convenciones, tenemos que editar el archivo config/war.rb para referenciar el conector.

1
2
  
maven_library 'mysql', 'mysql-connector-java', '5.0.4'

Teniendo en cuenta que por defecto Goldspike ejecutará la aplicación Rails en modo producción, tendremos que configurar adecuadamente ’’config/database.yml’’ Por último:


rake war:standalone:create

Tras un rato de funcionamiento la tarea de Rake finalizará dejando un bonito myapp.war en nuestro directorio. Obsérvese que el fichero .war pesa la friolera de 18M: no en vano incluye todo JRuby, Rails y las gemas que tuviéramos instaladas. Si no hemos parado la anterior instancia de JBoss podemos copiar en caliente el war:

cp myapp.war /Users/juan/jbossdemo/jboss-4.2.1.GA/server/default/deploy/

Veremos que en JBoss comienzan a pasar cosas y nuestra aplicación cobra vida.

23:29:10,762 INFO  [TomcatDeployer] deploy, ctxPath=/myapp, warUrl=.../tmp/deploy/tmp22213myapp-exp.war/
23:29:11,322 INFO  [[/myapp]] Ruby is running in standalone mode
23:29:14,472 INFO  [[/myapp]] JRuby init time: 2082ms
23:29:26,946 INFO  [[/myapp]] Rails init time: 9818ms
23:29:26,947 INFO  [[/myapp]] Runtime 0 loaded
23:29:26,947 INFO  [[/myapp]] Requests can now be processed
23:29:35,874 INFO  [[/myapp]] Runtime 1 loaded

Básicamente, JBoss descomprime el war en un directorio temporal y lanza dos instancias de Rails, lo que sería equivalente a dos mongrels tradicionales (por supuesto todo esto en configurable, según se explca en el wiki de Headius)

Tras esto, y si no ha habido errores (es de suponer que no) ¡es el momento de probar nuestra aplicación! Sólo debemos tener en cuenta que la URL raíz es, por la configuración de JBoss por defecto, algo distinta: http://127.0.0.1:8080/myapp, por lo que las URLs absolutas de nuestra aplicación (si las hubiera, que no debería) dejarán de funcionar.

Mingle es una aplicación escrita con Ruby on Rails. Hasta aquí, nada de extrardinario.

Lo interesante llega cuando nos cuentan algunas de sus características: será una aplicación de escritorio para entornos corporativos y que deberá correr sobre Windows, Linux y Mac.

Y más interesante aún es la plataforma sobre la que corre esta aplicación Rails: usa como base de datos Derby y Jetty como servidor web. Nada que ver con los habituales mongrels y MySQL.

A estas alturas ya no nos sorprende saber que Mingle también se distribuirá como un fichero .war que se puede desplegar en cuaqluier contenedor J2EE.

Todo esto es posible gracias a JRuby

¿Por qué la siguiente expresión en Ruby no da ningún error


irb(main):001:0> puts x=1 if x.nil?
1
=> nil

... mientras que la siguiente falla estrepitosamente?


irb(main):001:0> puts "z is not defined" if z.nil?
NameError: undefined local variable or method `z' for main:Object
        from (irb):1

la diferencia ocurre en el análisis sintáctico que realiza el intérprete, que cuando se encuentra con un identificador (en nuestro caso, x y z) debe determinar si se trata de un objeto o bien de una invocación de un método definido en el ámbito de la clase o módulo actual (z.nil podría referirse a un método z que devuelve un objeto del que comprobamos si es nulo o no)

En el primer ejemplo, el intérprete de Ruby ve que hacemos referencia a una variable x en la expresión x=1 de manera que asume que x es un objeto, al que se le puede invocar el método x.nil?

En el segundo caso no es así: la referencia a z ocurre dentro de una cadena -donde el intérprete no husmea en busca de información sintáctica-, de manera que cuando llegamos a if z.nil? Ruby asume que se trata de una función o un método definido en el ámbito actual. Al no encontrarlos, devuelve el mensaje de error que, una vez considerado lo anterior, resulta ser bastante descriptivo.

Podemos comprobar que este proceso ocurre en el análisis sintáctico del fichero (en lugar de en tiempo de ejecución del código) con el siguiente código:


    irb(main):001:0> x.class
    NameError: undefined local variable or method `x' for main:Object
            from (irb):1
    irb(main):002:0> if false         
    irb(main):003:1>   x=0
    irb(main):004:1> end
    => nil
    irb(main):005:0> x.class
    => NilClass

La línea x=0 jamás se ejecuta, pero sí que es tenida en cuenta a la hora de determinar que x debe tratarse como un objeto en lugar de como una invocación de método.

Desde que trabajo con Ruby on Rails de manera intensiva cada día, una de las cosas que más me incordia es abandonar el cómodo entorno de script/console para lanzar la (al menos para mí) menos cómoda interfaz de mysql La razón por la que me veo obligado a hacer esto con cierta frecuencia es para encontrar los ids de los objetos ActiveRecord que me interesan para investigar o depurar alguna funcionalidad. Cuando la aplicación tiene cientos de registros en las tablas, viene muy bien poder hacer consultas por el nombre aproximado con la construcción LIKE de SQL. ActiveRecord nos permite escribir nuestro propio SQL pero como soy un vago me resulta bastante tedioso escribir la condición completa con la sintaxis que pide Rails, así que normalmente ando saliendo de la consola, escribiendo una sentencia SELECT ... LIKE en mysql y volviendo a la consola con el id apuntado.

Hasta hoy, que me he terminado de cansar y he escrito un plugin que permite hacer sentencias LIKE desde ActiveRecord de manera dinámica.

Finders dinámicos en ActiveRecord

Los finders dinámicos de ActiveRecord son una de las primeras perlas de Ruby con las que uno se encuentra cuando investiga las tripas de Rails. Supongo que serán conocidos, pero no está de más repasarlos un poco.

Supongamos que tenemos una clase ActiveRecord mapeada sobre una tabla MyTable con los atributos atributo1, atributo2 y atributo3, sabemos que podemos escribir:


Mytable.find_by_id(87)

Y ActiveRecord tratará de encontrar el registro identificado con el id 87. No parece nada del otro mundo, porque entre otras convenciones ActiveRecord asume que la clave primaria en nuestras tablas será una columna llamada id, por lo que podría ser que existiese un método find_by_id en ActiveRecord::Base

Pero resulta algo más sorprendente toparse con que sentencias como:


Mytable.find_by_atributo1('Valor')

también funcionan. Aquí comienza a saborearse el dinamismo de Ruby, y podemos asumir que ActiveRecord, de manera astuta, construye tantas funciones Mytable::find_by_... como atributos tengamos en la tabla, que lanzarán la correspondiente consulta SQL a la base de datos.

Pero.. ¡un momento!

1
2
Mytable.find_by_atributo1_and_atributo3('Valor', 58)
Mytable.find_by_atributo1_and_atributo2_and_atributo3('Valor',58,'Valor3')

¡También! funcionan. Y sería ridículo pensar que ActiveRecord construye funciones de manera dinámica para todas las combinaciones posibles de los atributos (y, además, en cualquier orden que queramos)

Nuestro propósito es añadir más finders dinámicos, que en lugar de búsquedas exactas hagan búsquedas aproximadas, invocándose de la siguiente manera:

Mytable.find_like_atributo1('al')

Para hacerlo, tenemos que entender bien qué hace ActiveRecord con estas misteriosas funciones dinámicas…

La magia de method_missing

A estas alturas ya nos imaginamos que no se añaden métodos para cada atributo, sino que hay algún otro mecanismo actuando en este caso. Se trata de method_missing que es el método que ejecuta una clase Ruby cuando se le invoca un método que no tiene implementado. Aquí podeis ver otro uso muy creativo de method_missing

ActiveRecord se aprovecha de method_missing para articular los finders dinámicos. Si abrís el fichero lib/active_record/base.rb vereis que la clase ActiveRecord::Base define un method_missing muy especial (si no estais viendo ese código, hacedlo ahora: leer código del core de Rails es siempre un ejercicio inspirador). Este método se activa cuando llamamos a Mytable.find_by_attributo('valor'), y lo primero que hace es comprobar con una expresión regular si el método invocado tenía la pinta find_by o find_all_by, en cuyo caso pasa a construir una sentencia SQL de búsqueda según los atributos y parámetros recibidos. Parece evidente que nosotros tendremos que puentear este método y hacer algo parecido por nuestra cuenta pero construyendo sentencias SQL con el modificador LIKE.

Cómo construir nuestro propio finder

Lo primero es preparar un plugin, lo cual es tan fácil como crear un directorio find_like en vendor/plugins, y ahí escribiremos un fichero init.rb con el siguiente contenido:


require 'find_like'

A continuación, creamos el directorio vendor/plugins/find_like/lib y ahí pondremos el código de nuestro plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module ActiveRecord

class Base
  class << self
  
  private
      
  def construct_conditions_from_like_arguments(attribute_names, arguments)
    conditions = []
    attribute_names.each_with_index{ |name, idx| 
      conditions <<= "#{table_name}.#{connection.quote_column_name(name)} LIKE '%%" << arguments[idx] << "%%'"}
    [conditions.join(" AND "), *arguments[0..attribute_names.length]]
  end
      
  alias_method :previous_method_missing, :method_missing

  def method_missing(method_id, *arguments)
    if match = /find_(all_like|like)_([_a-zA-Z]\w*)/.match(method_id.to_s)
      finder = (match.captures.first == 'all_like' ? :find_every : :find_initial)
          
      attribute_names = extract_attribute_names_from_match(match)
      super unless all_attributes_exists?(attribute_names)
      conditions = construct_conditions_from_like_arguments (attribute_names, arguments)
      options = {:conditions => conditions}
      set_readonly_option!(options)
      send(finder, options)
    else
      previous_method_missing(method_id, *arguments)
    end
  end
end
end
end

El código más o menos es fácil de seguir: usamos class << self para abrir la clase ActiveRecord::Base y modificar sus propias tripas. Dado que estamos volviendo a escribir el método method_missing no querremos perder la funcionalidad ofrecida por el method_missing originalmente incluido con ActiveRecord::Base (el que veíamos antes en base.rb) así que simplemente haremos un alias a este método y lo llamaremos desde nuestro method_missing si detectamos que el método invocado no es uno de los que nosotros queremos controlar (find_like o find_all_like)

Tras esto, podemos lanzar una consola y comprobar que, por arte de birlibirloque, ya tenemos nuestros finders operativos:

1
2
3
4
5
6
7
8
9
Tamarindo:~/src/repos/dconrails juan$ script/console development
Loading development environment.

>> Algoritmo.find_all_like_nombre("oyal")

=> [#<Algoritmo:0x245c908 @attributes={"status"=>"BEGIN", "nombre"=>"RoyalRoadDemo"}>, #<Algoritmo:0x245c82c @attributes={"status"=>"RUN", "nombre"=>"RoyalRoad2">]

>> Algoritmo.find_all_like_nombre_and_status("oyal", "UN")
=> [#<Algoritmo:0x243dabc @attributes={"status"=>"RUN", "nombre"=>"RoyalRoad2"}>]

Acabo de toparme con el Ruby Developer Center de Yahoo!. Tiene una pinta un poco embrionaria todavía, pero aparte de contener un montón de enlaces que cualquier desarrollador Rails ya conoce tiene algunos articultos interesantes sobre JSON, XML y su uso con los servicios web de Yahoo!.

Se han publicado recientemente tres artículos interesantes que versan sobre los símbolos en Ruby (esas curiosas entidades sintácticas que empiezan por dos puntos y que tan frecuentemente nos encontramos al programar en Rails).

Mi imagen mental sobre los símbolos era pensar en ellos como si fuesen cadenas globales e inmutables, pero Jim Weirich se encarga de arrojar algo de luz sobre el tema, e incluso Austin Ziegler responde con más detalle

Por último, hoy me encuentro con este otro artículo de Steve Yegge que no sólo sugiere otros usos futuros posibles de los símbolos, sino que además lo hace aplicando técnicas de metaprogramación que vendrán muy bien para retorcer las mentes de quienes, como yo, venimos de C++ (o Java).