Acceder a una base de datos existente en Sinatra

13 Jan
Published by ApuX in

Tags 

Off Topic

Acceder a una base de datos existente en Sinatra

Recientemente tuve la necesidad de utilizar Sinatra para construir una pequeña aplicación que realizara accediera a una base de datos de una aplicación Rails para consultar y modificar información.

Para los que no lo han utilizado, Sinatra es un framework muy fácil de usar y bastante potente al mismo tiempo. En su página, el código de ejemplo más básico que ponen como ejemplo es el siguiente:

require 'sinatra'
 
get '/hi' do
  "Hello World!"
end

El código es bastante simple y fácil de entender. Con ese fragmento de código nuestra aplicación soporta conexiones http get a la ruta /hi.

Para ejecutar nuestra aplicación, basta con hacer lo siguiente:

$ gem install sinatra
$ ruby hi.rb
== Sinatra has taken the stage ...
>> Listening on 0.0.0.0:4567

Con esto tenemos ya un servidor escuchando en el puerto 4567, podemos comprobarlo accediendo con nuestro navegador a localhost:4567/hi.

En nuestro ejemplo, no crearemos una base de datos nueva, sino que nos conectaremos a una base de datos ya existente, por lo que no necesitamos incluir rake ni realizar migraciones.

Para conectarse con la base de datos de la aplicación Rails se necesita de ActiveRecord y la gema correspondiente para la conexión con el manejador de base de datos, en mi caso postgres. Como podemos ver en el código de ejemplo, para trabajar con una gema en sinatra basta con tenerla instalada. En mi caso, prefiero utilizar bundler para administrar mis gemas, por lo que es necesario crear un archivo Gemfile.

source 'https://rubygems.org'
 
gem "sinatra"                  
gem "pg"
gem "activerecord"             
gem "sinatra-activerecord"

Ahora podemos hacer bundle install para instalar esas gemas.

Las gemas agregadas son: sinatra, que es la gema del framework; pg, que es la gema para la conexión a postgresql; activerecord que es la gema principal de activerecord; y sinatra-activerecord que es una gema que permite la integración de activerecord en sinatra.

Para nuestra pequeña aplicación, trabajaremos con un sólo archivo: app.rb, a la que agregaremos el siguiente código:

require 'sinatra'
require 'sinatra/activerecord'

Desafortunadamente, cada cambio que hagamos en el código requiere detener nuestra aplicación y volver a ejecutarla. Si prefieres que se recargue automáticamente, la gema sinatra-contrib incluye esa funcionalidad, sólo tienes que incluir require "sinatra/reloader" en el archivo app.rb

Para conectare a la base de datos, es necesario especificar los parámetros de conexión. Esto se puede hacer directamente el código ruby o bien mapear a un archivo yml. En nuestro caso, haremos lo segundo.

set :database_file, "database.yml"

Nuestro archivo database.yml sería muy similar al que utilizamos en Rails.

development: adapter: postgresql encoding: unicode database: database_dev pool: 5 username: x password: x

Con nuestra conexión a base de datos configurada, ahora tenemos que trabajar con ActiveRecord para acceder a la información necesaria. Para nuestro ejemplo, trabajaremos con tres modelos: Account, User y Project. Para que el ejemplo sea un poco más completo, el modelo Account estará dentro de un módulo Suscription, tal como en nuestra aplicacion Rails.

A diferencia de Rails, sinatra no presupone una estructura de archivos para los modelos, por lo que, por comodidad, pondremos todas los modelos en el archivo app.rb. Por supuesto, si el número de modelos fuera mayor, es posible distribuirlos en sus propios archivos.

Nuestro modelo Project lo podemos definir de forma muy sencilla

class Project < ActiveRecord::Base
  belongs_to :user
end

El modelo User tambien es muy similar a como seria en Rails.

class User < ActiveRecord::Base
  belongs_to :account, class_name: 'Suscription::Account'
  has_many :projects
end

Con el modelo Account tenemos un detalle a tomar en cuenta: debe ser declarado dentro de un módulo y el nombre de la tabla no es accounts sino suscription_accounts. Ahora bien, el modulo Suscription existe en nuestra aplicacion Rails, pero no tiene forzosamente que existir en nuestra aplicacion sinatra, aquí podemos mapear nuestros objetos de forma diferente. Sin embargo, por claridad de codigo, utilizaremos el módulo tal como en nuestra aplicación Rails.

Creamos el modelo Account indicando el nombre de la tabla a la que debe conectarse. Anteriormente eso se hacía con el método set_table_name pero ahora se hace uso del atributo table_name, al que se le asigna el valor de la tabla.

module Suscription
  class Suscription::Account < ActiveRecord::Base
    self.table_name = 'suscription_accounts'
    has_many :users
    has_many :projects, through: :users
  end
end

Ahora sí, con nuestros modelos correctamente mapeados, podemos hacer uso de ellos como lo hacemos en rails. Podemos usar validaciones como presence, uniqueness, etc; callbacks como after_initialize, before_create, etc, o scopes para simplificar nuestras consultas, etc.

Nuestros modelos quedan de la siguiente manera:

class Project < ActiveRecord::Base
  belongs_to :user
  scope :active, -> { where running: true }
end
 
class User < ActiveRecord::Base
  belongs_to :account, class_name: 'Suscription::Account'
  has_many :projects
end
 
module Suscription
  class Account < ActiveRecord::Base
    self.table_name = 'suscription_accounts'
    has_many :users
    has_many :projects, through: :users
 
    def active_projects_count
      @active_projects_count ||= projects.active.count
    end
 
    def increment_projects_limit(projects_to_add)
      update projects_limit: projects_limit + projects_to_add.to_i
    end
  end
end

El modelo Account es el que agrega la funcionalidad que se utilizará en el sistema, agregando el métodoactive_projects_count, que cuenta el número de proyectos activos, y el método increment_projects_limit que aumenta el límite de proyectos por cuenta.

Por último, agregamos las rutas y la funcionalidad en las que accederemos a nuestra aplicación:

get "/accounts" do
  @accounts = Suscription::Account.all
  erb :index
end
 
get "/accounts/:id/edit" do
  @account = Suscription::Account.find(params[:id])
  erb :edit
end
 
get "/accounts/:id" do
  @account = Suscription::Account.find(params[:id])
  erb :show
end
 
post "/accounts/:id" do
  @account = Suscription::Account.find(params[:id])
  if @account.increment_projects_limit(params[:account][:projects_to_add])
    erb :show
  else
    erb :edit
  end
end

Las rutas que hemos creado son cuatro: los equivalentes al index, edit, show y update de Rails. El código es bastante claro y no creo que merezca más explicacion.

Como podemos ver, las respuestas las delegamos a erb, por lo que necesitamos crear los archivos erb en el proyecto. Estos archivos deben incluirse en la carpeta views. A diferencia de rails, la extension de los archivos no deben ser .html.erb, sino simplemente .erb.

El archivo views/index.erb

<table border="0">
  <thead>
    <tr>
      <th>Subdomain</th>
      <th>Projects Limit</th>
      <th>Options</th>
    </tr>
  </thead>
  <tbody>
    <% @accounts.each do |account| %>
      <tr>
        <td><%= account.subdomain %></td>
        <td><%= account.projects_limit %></td>
        <td>
          <a href="/accounts/<%= account.id %>">Show</a>
          <a href="/accounts/<%= account.id %>/edit">Edit</a>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

El archivo views/show.erb

<p><b>Subdomain: </b><%= @account.subdomain %></p>
<p><b>Projects limit: </b><%= @account.projects_limit %></p>
<p><b>Active projects: </b><%= @account.active_projects_count %></p>
 
<a href="/accounts/<%= @account.id %>/edit">Edit</a>

El archivo views/edit.erb

<form method="post" action="/accounts/<%= @account.id %>">
  <p>
    <b>Subdomain: </b>
    <input name="account[subdomain]" value="<%= @account.subdomain %>" %>
  </p>
  <p>
    <b>Number of projects to be added: </b>
    <input name="account[projects_to_add]" >
  </p>
  <input type="submit" value="Submit">
</form>

En nuestro ejemplo hemos utilizado erb, pero tambien es posible utilizar otra heramienta de templates como haml.

Para ejecutar nuestra aplicación, basta con ejecutar nuestro archivo app.rb.

ruby app.rb

Por supuesto, no se han contemplado todos los flujos, no se muestran los errores si es que los hay, ni se provee de un mecanismo para actualizar otros campos, pero para fines de nuestro ejemplo, con esto completamos nuestra pequeña aplicación Sinatra que consulta y actualiza una base de datos que tambien es accedida una aplicacion en Rails.