09: ACTIVE RECORD: Active Record Avanzado - LPC-Ltda/Ruby-on-Rails GitHub Wiki

El debate respetuoso, la honestidad, la pasión, y los sistemas de trabajo crearon un ambiente que ni siquiera el más dedicado arquitecto de la empresa podía pasar por alto, no importa cuán enterrado estuviera en los patrones de diseño Java. Aquellos que ponen la excelencia técnica y el pragmatismo por encima de apego religioso y la amistad del proveedor fueron fácilmente convencidos de los beneficios que la ampliación de su definición de tecnologías aceptables podría traer. -Rayn Tomayko

Active Record es un framework de mapeo relacional de objetos simple (ORM) comparado con otros frameworks ORM, como Hibernate en el mundo Java. No dejes que eso te engañe: Bajo su modesto exterior, Active Record tiene algunas características muy avanzadas. Para realmente obtener la mayor eficacia del desarrollo Rails, usted necesita tener más que un entendimiento básico de Active Record. -cosas como cuando romper con el patrón una-tabla/una-clase o cómo usar los módulos de Ruby para codificar en forma limpia y libre de duplicaciones.

En este capítulo, envolvemos la cubierta comprensiva de Active Record de este libro para revisar callbacks, single-table inheritance (STI), y los modelos polimórficos. También revisamos un poco de información acerca de la metaprogramación y los lenguajes de dominio específico de Ruby como ellos se relacionan con Active Record.

9.1 Scopes

Los scopes (o "named scopes" si usted es de la vieja escuela) le permite definir y encadenar criterios de consulta en una forma declarativa y reusable.

class Timesheet < ActiveRecord::Base
  scope :submited, -> { where(submitted: true) }
  scope :underutilized, -> { where('total_hours < 40') }

Para declarar un scope, use el método de clase scope, pásele un nombre como símbolo y un objeto llamable que contine un criterio de consulta dentro de él. Usted puede usar los métodos de criterio Arel, tales como where, order y limit para construir la definición como se muestra en el ejemplo. Las consultas definidas en un scope son solo evaluadas cuando el scope es invocado.

class User < ActiveRecord::Base
 scope :delinquent, -> { where('timesheets_updated_at < ?', 1.week.ago) }

Invoque los scopes como lo haría con los métodos de clase.

>> User.delinquent
=> [#<User id: 2, timesheets_updated_at: "2013-04-20 20:02:13" ...>]

Note que en lugar de usar el método estilo macro scope, usted puede simplemente definir un método de clase sobre un modelo Active Record que retorna un método scopeado, tal como where. Para ilustrar, el siguiente método de clase es equivalente al scope delinquent definido en el ejemplo previo.

def self.delinquent
  where('timesheets_updated_at < ?', 1.week.ago)
end

9.1.1 Parámetros de scope

Usted puede pasar argumentos a la invocación de un scope agregando parámetros al proc que usted usa para definir la consulta scope.

class BillableWeek < ActiveRecord::Base
  scope :newer_than, ->(date) { where('start_date > ?', date) }

Entonces pase el argumento al scope como usted lo haría normalmente.

BillableWeek.newer_than(Date.today)

9.1.2 Encadenando scopes

Uno de los beneficios de los scopes es que usted puede juntarlos para crear consultas desde otras más simples:

>> Timesheet.underutilized.submitted.to_a
=> [#<Timesheet id: 3, submitted: true, total_hours: 37 ...]

Los scopes pueden ser encadenados juntos para ser reusados dentro de las definiciones de scope mismas. Por ejemplo, digamos que deseamos restringir siempre el conjunto resultado del scope underutilized a sólo los timesheets emitidos:

class Timesheet < ActiveRecords::Base
  scope :submitted, -> { where(submitted: true) }
  scope :underutilized, -> { submitted.where('total_hours < 40') }

9.1.3 Scopes y has_many

Adicionalmente a estar disponible en el contexto de clase, los scopes están disponibles automáticamente sobre los atributos de asociaciones has_many.

>> u = User.find(2)
=> #<User id: 2, username: "obie" ...>
>> u.timesheet.size
=> 3
>> u.timesheets.underutilized.size
=> 1

9.1.4 Scopes y Joins

Usted puede usar los métodos join de Arel para crear scopes inter modelos. Por ejemplo, si tenemos nuestro ejemplo recurrente Timesheet un atributo date submitted_at en lugar de sólo un booleano, podemos agregar un scope a User que nos permite ver quien está atrasado en el envío de sus timesheets.

scope :tardy, -> {
  joins(:timesheets).
  where("timesheets.submitted_at <= ?", 7.days.ago).
  group("users.id")
}

El método to_sql de Arel es útil hacer debugg de la definición del scope y su uso.

>> User.tardy.to_sql
=> "SELECT "users".* FROM "users"
  INNER JOIN "timesheets" ON "timesheets"."user_id" = "users"."id"
  WHERE (timesheets.submitted_at <= '2013-04-13 18:16:15.203293')
  GROUP BY users.id"  # query formatted nicely for the book

Note que como es demostrado en el ejamplo, es una buena idea usar referencias de columnas no ambiguas (incluir el nombre de la tabla) en la definición de scopes inter modelos de manera que Arel no se confunda.

9.1.5 Combinaciones de scope

Nuestro ejemplo de scope inter-modelo viola los principios de un buen diseño orientado al objeto: contiene la lógica para determinar si en envía o no un Timesheet, lo cual es código que pertenece en propiedad al la clase Timesheet. Afortunadamente, podemos usar el método merge de Arel para solucionarlo. Primero, ponemos ponemos la lógica donde pertenece -en Timesheet:

scope :late, -> { where("timesheet.submited_at <= ?", 7.days.ago) }

Luego usamos nuestro nuevo scope late en tardy:

scope :tardy, -> {
  joins(:timesheets).group("users.id").merge(Timesheet.late)
}

Si usted tiene problemas con esta técnica, asegúrese de que las cláusulas de sus scopes se refieran a nombres de columnas completamente calificados. (En otras palabras, no olvide prefijar los nombres de columnas con las tablas) La consola y el método to_sql son sus amigos para hacer el debugg.

###9.1.6 Scopes por defecto

Pueden surgir casos donde usted desea ciertas condiciones aplicadas a los buscadores para su modelo. Considere que nuestra aplicación timesheet tiene una vista pr defecto de timesheets abiertos -podemos usar un scope por defecto para simplificar nuestras consultas generales.

class Timesheet < ActiveRecord::Base
  default_scope { where(status: open) }
end

Ahora cuando consultemos por nuestras Timesheets, por defecto, la condición abierta será aplicada:

>> Timesheet.pluck(:status)
=> ["open", "open", "open"]

Los scopes por defecto son también aplicados a sus modelos cuando los construye o crea, lo cual puede ser muy conveniente o una molestia si usted no tiene cuidado. En nuestro ejemplo previo, todo los nuevos Timesheets serán creados con el status "open".

>> Timesheet.new
=> #<Tmesheet id: nil, status: "open">
>> Timesheet.create
=> #<Tmesheet id: 1, status: "open">

Usted puede sobre-escribir este comportamiento entregando sus propias condiciones o scope para sobre-escribir el seteo por defecto de los atributos.

>> Timesheet.where(status: "new").new
=> #<Tmesheet id: nil, status: "open">
>> Timesheet.where(status: "new").create
=> #<Tmesheet id: 1, status: "open">

Puede haber casos en los que en tiempo de ejecución usted desea crear un scope y pasarlo al rededor como un objeto de primera clase aprovechando su scope por defecto. En este caso, Active Record provee el método all.

>> timesheets = Timesheet.all.order("submitted_at DESC")
=> #<ActiveRecord::Relation [#<Timesheet id: 1, status: "open"]>
>> timesheets.where(name: "Durran Jordan").to_a
=> []

Hay otra aproximación a los scopes que provee una sintaxis más elegante: scoping, la cual le permite encadenar scopes al anidar dentro de un block.

>> Timesheet.order("submitted_at DESC").scoping do
>>    Timesheet.first
>> end
=> #<Timesheet id: 1, status: "open">

Esto es simpático, pero que pasa si no necesitamos que nuestro scope por defecto sea incluido en nuestras consultas? En este caso, Active Record cuida de nosotros a través del método unscoped.

>> Timesheet.unscoped.order("submitted_at DESC").to_a
=> [#<Timesheet id: 2, status: "submited">]

Similarmente para sobre-escribir nuestro scope por defecto con una relación cuando creamos nuevos objetos, podemos aplicar unscoped también para remover los atributos por defecto.

>> Timesheet.unscoped.new
=> #<Timesheet id: nil, status: nil>

###9.1.7 Usando scopes para CRUD

Usted tiene un amplio rango de métodos CRUD de Active Record disponibles sobre scopes, los cuales le dan algunas habilidades poderosas, Por ejemplo, demosle a todas nuestras timesheets subutilizadas algunas horas extras.

u.timesheets.underutilized.pluck(:total_hours)
=> [37, 38]

u.timesheets.underutilized.update_all("total_hours = total_hours + 2")
=> 2

u.timesheets.underutilized.pluck(:total_hours)
=> [39]

Los scopes -incluida la cláusula where usando condiciones en hash- puede poblar atributos de objetos construidos fuera de ellos con estos atributos como valores por defecto. Es cierto, es un poco difícil pensar un caso de uso para esta característica, pero la mostraremos en un ejemplo. Primero, agregamos el siguiente scope a Timesheet:

scope :perfect, -> { submitted.where(total_hours: 40) }

Ahora construimos un objeto sobre el scope perfect que nos pueda dar un timesheet enviado con 40 horas.

>> Timesheet.perfect.build
=> #<Timesheet id: nil, submitted: true, user_id: nil, total_hours: 40 ...>

Como usted probablemente notó, el apoyo de Arel para Active Record es tremendamente poderoso y realmente eleva la plataforma Rails.

##9.2 Callbacks

Esta característica avanzada de Active Record le permite al desarrollador experto adjuntar comportamiento en una variedad de puntos diferentes a lo largo del ciclo de vida de un modelo, como después de la inicialización; antes de que el registro de la base de datos sea insertado, actualizado o removido, y así.

Los callbacks pueden hacer una variedad de tareas, que van desde cosas simples como el logeo y el masajeo de valores de atributos antes de la validación de un cálculo complejo. Los callbacks pueden detener la ejecución del proceso del ciclo de vida que está tomando lugar. Algunos callbacks pueden incluso modificar el comportamiento de la clase modelo durante la ejecución. Cubriremos todos estos escenarios en esta sección, pero primero miremos como luce un callback. Mire el siguiente ejemplo tonto:

class Beethoven < ActiveRecord::Base
  before_destroy :last_words

  protected

  def last_words
    logger.info "Friends applaud, the comedy is over"
  end
end

Así antes de morir (ser destruida), las últimas palabras de la clase Beethoven serán siempre logeadas para la posteridad. Como veremos pronto, hay 14 diferentes oportunidades para agregar comportamiento a su modelo en esta forma. Antes de irnos a esta lista, cubriremos los mecanismos de registrar un callback.

###9.2.1 De línea única (one-liners)

Ahora sí (y sólo sí) su rutina callback es realmente corta, usted puede agregarla pasando un block a la macro callback.

class Napoleon < ActiveRecord::Base
  before_destroy { logger.info "Josephine..." }
  ...
end

A partir de Rails 3, el block pasado al callback es ejecutado vía instance_eval así que su scope es el registro mismo (versus necesitar actuar sobre un registro pasado como variable). El siguiente ejemplo implementa el comportamiento del modelo "paranoid", cubierto después en este capítulo.

class Account < ActiveRecord::Base
  before_destroy { self.update_attribute(:deleted_at, Time.now); false }

###9.2.2 Protected o Private

Exepto cuando usted está usando un block, el nivel de acceso para métodos callbacks debe ser siempre protected o private. Jamás debe ser public, ya que los callbacks nunca deben ser llamados desde código fuera del modelo.

Créalo o no, hay incluso más formas de implementar callbacks, pero cubriremos estas técnicas más tarde en este capítulo. Por ahora, miremos una lista de engenches de callbacks disponibles.

###9.2.3 Callbacks before/after

En total, hay 19 tipos de callbacks que usted puede registrar en sus modelos! trece de ellos coinciden con el par de callbacks before/after, como los before_validation y after_validation. Cuatro de ellos son callbacks around, tales como around_save. (Los otros dos, after_initialize y after_find son especiales, y los discutiremos luego en esta sección).

9.2.3.1 Lista de callbacks

Esta es la lista de enganches callbacks disponibles durante la operación save. (La lista varía levemente dependiendo de si usted está grabando un registro nuevo o uno ya existente).

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create (para registros nuevos) y before_update (para registros existentes)
  • around_create (para registros nuevos) y around_update (para registros existentes)
  • after_create (para registros nuevos) y after_update (para registros existentes)
  • after_save

Las operaciones de borrado tienen sus propios callbacks:

  • before_destroy
  • around_destroy el cual ejecuta una instrucción de base de datos DELETE sobre yield
  • after_destroy, el cual es llamado después que e registro ha sido removido de la base de datos y todos los atributos han sido congelados (readonly).

Los callbacks pueden ser limitados a un ciclo de vida específico de Active Record (:create, :update, :destroy) definiendo explícitamente cuales gatillarlo usando la opción :on. La opción :on puede aceptar un único ciclo de vida (como on: :create) o un arreglo de ciclos de vida (como on: [:create, :update]).

# Run only on create
before_validation :some_callback, on: :create

Adicionalmente, las transacciones tienen callbacks para cuando quieras que las acciones ocurran después de que esté garantizado que la base de datos esté en un estado permanente. Note que sólo existen aqui callbacks "after" debido a la naturaleza de las transacciones -es una mala idea estar habilitado a interferir con la operación actual misma.

  • after_commit
  • after_rollback
  • after_touch

Saltarse la ejecución de un callback. Los siguientes métodos de Active Record no corren ningún callback cuando son ejecutados:

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • increment
  • increment_counter
  • toggle
  • touch
  • update_column
  • update_columns
  • update_all
  • update_counters

###9.2.4 Detener la ejecución

Si usted retorna un false (no nil) desde un método callback, Active Record detiene la cadena de ejecución. No más callbacks son ejecutados. El método save retornará false y save! levantará un error RecordNotSaved.

Tenga en mente que ya que la última expresión de un método Ruby es retornada implícitamente, es un error muy común escribir un callback que detiene la ejecución accidentalmente. Si usted tiene un objeto con callbacks que misteriosamente falla al grabar, asegúrese que no esté retornando false por error.

###9.2.5 Usos de los callbacks

Por supuesto, los callbacks que usted debe usar para una situación dada depende de lo que usted está tratando de lograr. Lo mejor que puedo hacer es entregarle algunos ejemplos para inspirarlo con su propio código.

9.2.5.1 Limpiando el formateo de atributos con before_validation sobre create

Los ejemplos más comunes del uso del callback before_validation tiene que ser la limpieza de atributos ingresados por el usuario. Por ejemplo, la siguiente clase CreditCard limpia su atributo number para que no ocurra una falsa negativa en la validación:

class CreditCard < ActiveRecord::Base
  before_validation on: create do
    self.number = number.gsub(/[^0-9]/, "")
  end
end

9.2.5.2 Geocodifcando con before_save

Asumamos que usted tiene una aplicación que da seguimiento a direcciones y tiene características de mapeo. Las direcciones deben ser siempre geocodificadas antes de grabarlas para que puedan ser rápidamente desplegadas después. (Recomiendo la excelente gema geocodificadora en http://www.rubygeocoder.com).

Como es frecuentemente el caso, la explicación del requerimiento lo encamina en la dirección del callback before_save:

class Address < ActiveRecord::Base
  before_save :geocode
  validates_presence_of :street, :city, :state, :country
  ...

  def to_s
    [street, city, state, country].compac.join(', ')
  end

  protected

  def geocode
    result = Geocoder.coordinates(to_s)
    self.latitude = result.first
    self.longitude = result.last
  end
end

Nota: En aras de este ejemplo, no estamos usando las extensiones Active Record de Geocoder.

Antes de seguir adelante tenemos un par de consideraciones adicionales. El código anterior trabaja genial si el geocoder tiene éxito, pero que pasa si no? Desearemos todavía entregar el registro para ser grabado? Si no, podemos detener la cadena de ejecución:

  def geocode
    result = Geocoder.coordinates(to_s)
    return false if result.empty?  # halt execution

    self.latitude = result.first
    self.longitude = result.last
  end

El único problema que permanece es que no le dimos al resto del código (y por ende al usuario final) ninguna indicación del porque la cadena fue detenida. Aunque no estamos en una rutina de validación, pienso que podemos poner la colección errors para buenos usuarios aquí:

  def geocode
    result = Geocoder.coordinates(to_s)
    if result.present?
      self.latitude = result.first
      self.longitude = result.last
    else
      errors[:base] << "Geocodding failed. Please check address."
      false
    end
  end

Si el geocoding falla, agregamos un mensaje de error de base (para todo el proyecto) y detenemos la ejecución así que el registro no es grabado.

**9.2.5.3 Ejercite su Paranoia con before_destroy

Que pasa si su aplicación tiene que manejar un tipo importante de data que, una vez ingresada, nunca debe ser borrada? Quizá para esto hace sentido colgarse del mecanismo destroy y de alguna manera marcar el registro como borrado a cambio?

El siguiente ejemplo depende de la tabla accounts teniendo una columna delete_at.

class Account < ActiveRecord::Base
  before_destroy do
    self.update_attribute(:delete_at, Time.current)
    false
  end

  ...
end

Después de llenar la columna deleted_at con la hora actual, corremos false en el callback para detener la ejecución. Esto asegura que el registro subyacente no es realmente borrado de la base de datos.

Implementaciones de la vida real del ejemplo puede también necesitar modificar todos los buscadores para incluir condiciones donde delete_at es nulo; de otra forma, los registros marcados delete pueden continuar mostrándose en la aplicación. Esta no es una empresa trivial, y afortunadamente usted no necesita hacerlo por usted mismo. Existe un plugin Rails llamado destroyed_at creado por Dockyard que hace exactamente eso y usted puede encontrarlo en https://github.com/dockyard/destroyed_at

Probablemente vale la pena mencionar que hay formas en que Rails le permite eludir involuntariamente los callbacks before_destroy:

  • Los métodos de clase delete y delete_all de ActiveRecord::Base son casi idénticos. Ellos remueven filar directamente de la base de datos sin hacer instancias de la correspondiente instancia del modelo, lo cual significa que los callback no ocurrirán.
  • Objetos de modelos en asociaciones definidas con la opción dependent: :delete_all serán borradas directamente desde la base de datos cuando sea removida de la colección usando los métodos de asociación clear o delete.

9.2.5.4 Limpiando archivos asociados con after_destroy

Los objetos de modelos que tienen archivos asociados con ellos, tales como un registro de attachment e imagenes subidas, pueden limpiar limpiarlos cuando sea borrado usando el callback after_destroy. El siguiente método de la gema Paperclip (https://github.com/thoughbot/paperclip) de Thoughbot es un buen ejemplo:

# Destroy the file. Called in an after_destroy callback.
def destroy_attached_files
  Paperclip.log("Deleting attachment.")
  each_attachment do |name, attachment|
    attachment.send(:flush_deletes)
  end
end

###9.2.6 Callbacks especiales after_initialize y after_find

El callback after_initialize es invocado donde quiera que un modelo Active Record es instanciado (ya sea desde cero o desde la base de datos). Tenerlo disponible le impide tener que enrredarse con la sustitución del método initialize actual.

El callback after_find es invocado donde quiera que Active Record carga un objeto del modelo desde una base de datos y se llama antes que after_initialized si ambos son implementados. Ya que tanto after_find como after_initialize son llamados por cada objeto encontrado e instanciado por los buscadores, las restricciones de desempelño dictan que ellos pueden sólo ser agregados como métodos y no vía macros de callback.

Que pasa si usted desea ejecutar algo de código la primera vez que un objeto es instanciado y no cada vez que es cargado desde la base de datos? No hay callbacks nativos para ese escenario, pero usted puede hacerlo usando un callback after_initialize. Sólo agregue una condición que chequee ver si es un registro nuevo:

after_initialize do
  if new_record?
    ...
  end
end

En un número de aplicaciones Rails que he escrito, he encntrado útil capturar las preferencias del usuario en un hash serializado asociado con el objeto User. La característica serialize de los modelos Active Record hace esto posible, ya que registran transparentemente graphos de objetos Ruby en columnas de texto dentro de la base de datos. Desafortunadamente, usted no puede pasarles un valor por defecto, así que tengo que setearlos por mi mismo:

class User < ActiveRecord::Base
  serialize :preferences # defaults to nil
  ...

  protected

  def after_initialize
    self.preferences ||= Hash.new
  end
end

Usando el callback after_initialize, puedo automáticamente poblar el atributo preferences de mi modelo user con un hash vacío, así nunca deberé preocuparme de que sea nil cuando acceda a él con código como `user.preferences[:show_help_text] = false.

Usted puede cambiar el ejemplo previo para no usar callbacks usando el store de Active Record, un envoltorio de serialize que es usado exclusivamente para almacenar hashes en una columna de base de datos.

class User < ActiveRecord::Base
  serialize :preferences # defaults to nil
  store :preferences, accesors: [:show_help_text]
  ...
end

Por defecto el atributo preferences será llenado con un hash vacío. Otro beneficio agregado es la habilidad para definir explícitaente accesors. eliminando la necesidad de interactuar directamente con el hash subyacente. Para ilustrar setiemos la preferencia show_help_text a true:

>> user = User.new
=> #<User id: nil, properties: {}, ...>
>> user.show_help_text = true
=> true
>> user.properties
=> {"show_help_text" => true}

Las capacidades de metaprogramación de Ruby combinadas con la habilidad de ejecutar código donde quiera que se cargue el modelo usando el callback after_find son una mezcla poderosa. Ya que aún no hemos terminado el aprendizaje acerca de los callbacks aún, volveremos sobre el uso de after_find después en este capítulo en la sección "Modificando clases Active Record en tiempo de ejecución".

###9.2.7 Clases callback

Es suficientemente común desear reusar código callback para más de un objeto que Rails provee una forma de escribir clases callback. Todo lo que usted tiene que hacer es pasar a un queue de callback dado un objeto que responda al nombre del callback y que tome el objeto modelo como parámetro.

Aquí está nuestro ejemplo paranoid de la sección previa como una clase callback:

class MarkDeleted
  def self.before_destroy(model)
    model.update_attribute(:deleted_at, Time.current)
    false
  end
end

El comportamiento de MarkDeleted es apátrida, así agregue el callback como un método de clase. Ahora usted no tiene que instanciar objetos MarkDeleted por malas razones. Todo lo que usted hace es pasar la clase al queue de callback para cualquier modelo que usted desee que tenga el comportamiento mark-deleted:

class Account < ActiveRecord::Base
  before_destroy MarkDeleted
  ...
end

clas Invoice < ActiveRecord::Base
  before_destroy MarkDeleted
  ...
end

9.2.7.1 Múltiples métodos callback en una clase

No hay regla que diga que usted no pueda tener más de un método callback en una clase callback. Por ejemplo, usted puede tener requerimientos de auditoria de logs especiales para impementar:

class Auditor
  def initialize(audit_log)
    @audit_log = audit_log
  end

  def after_create(model)
    @audit_log.created(model.inspect)
  end

  def after_update(model)
    @audit_log.destroyed(model.inspect)
  end

  def after_destroy(model)
    @audit_log.destroyed(model.inspect)
  end
end

Para agregar audit logging a una clase Active Record, usted debe hacer lo siguiente:

class Account < ActiveRecord::Base
  after_create Auditor.new(DEFAULT_AUDIT_LOG)
  after_update Auditor.new(DEFAULT_AUDIT_LOG)
  after_destroy Auditor.new(DEFAULT_AUDIT_LOG)
end

Wow, esto es feo, tener que agregar tres Auditors en tres líneas. Podemos extraer una variable local llamada auditor, pero esto será aún repetitivo. Esta puede ser una oportunidad de sacar ventaja de las clases abiertas de Ruby, que le permiten clases que no son parte de su aplicación.

No sería mejor decir acts_as_audited en la cima del modelo que desea ser auditado? Podemos rápidamente agregarlo a la clase ActiveRecord::Base así estará disponible para todos nuestros modelos.

En mi proyecto, el archivo fue codificado "rápido y sucio" como el método en el Listado 9.1 que reside en lib/core_ext/active_record_base.rb, pero usted puede ponerlo donde desee. Usted puede incluso hacerlo un plugin.

Listado 9.1 Método acts_as_audited Rápido y sucio

class ActiveRecord::Base
  def self.acts_as_audited(audit_log=DEFAULT_AUDIT_LOG)
    auditor = Auditor.new(audit_log)
    after_create auditor
    after_update auditor
    after_destroy auditor
  end
end

Ahora la cima de Account es mucho menos desordenada:

class Acount < ActiveRecord::Base
  acts_as_audited

9.2.7.2 Comprobabilidad

Cuando usted agrega métodos callback a una clase modelo, usted tiene que probar bastante bien si están funcionando correctamente en conjunto con el modelo al cual fueron agregadas. Esto puede o no ser un problema. En contraste, las clases callback son simples de probar aisladas.

describe '#after_ceate' do
  let(:auditable) { double() }
  let(:log) { double() }
  let(:content) { 'foo' }

  it 'audits a model was created' do
    expect(auditable).to receive(:inspect) .and_return(content)
    expect(log).to receive(:created) .and_return(content)
    Auditor.new(log).after_create(auditable)
  end
end

##9.3 Métodos de cálculo

Todas las clases Active Record tienen un método calculate que provee acceso fácil a consultas de función agregada en las bases de datos. Métodos para count, sum, average, minimum y maximum han sido agregados con abreviaciones convenientes.

Los métodos de cálculo pueden ser usados en combinación con los métodos de relación para personalizar la consulta. Ya que los métodos de cálculo no retornan una ActiveRecord::Relation, deben ser el último método en una cadena de scope.

Hay dos formas básicas de salida:

Valor agregado simple El único valor es llevado a Fixnum para COUNT, Float para AVG, y al tipo de columna dado para todo lo demás.

Valores agrupados Esto retorna un hash ordenado de los valores y los agrupa por la opción :group. Toma ya sea el nombre de la columna o el nombre de la asociación belongs_to.

El siguiente ejemplo ilustra el uso de varios métodos de cálculo.

Person.calculate(:count, :all) # the same as Person.count

# SELECT AVG(age) FROM people
Person.average(:age)

# Selects the minimum age for everyone with a last name other than "Drake."
Person.where.not(last_name: 'Drake').minimum(:age)

# Selects the minimum age for any family without any minors.
Person.having('min(age) > 17').group(:last_name).minimum(:age)

###9.3.1 average(column_name, *options)

Calcula el valor promedio de una columna dada. El primer parámetro debe ser un símbolo que identifica la columna que se promediará.

###9.3.2 count(column_name, *options)

Count opera usando tres diferentes aproximaciones. Count sin parámetros retornará una cuenta de todas las filas en un modelo. Count con column_name retornará la cuenta de todas las filas del modelo con dicha columna presente.

###9.3.3 ids

Retorna todos los ids para una relación basada en su llave primaria de tabla:

User.ids # SELECT id FROM "users"

###9.3.4 maximum(column_name, *options)

Calcula el máximo valor de una columna dada. El primer parámetro debe ser un símbolo que identifique la columna a calcular.

###9.3.5 minimum(column_name, *options)

Calcula el mínimo valor de una columna dada. El primer parámetro debe ser un símbolo que identifique la columna a calcular.

###9.3.6 pluck(*column_name)

El método de pluck consulta la base de datos por una o más columnas del la tabla del modelo subyacente.

>> User.pluck(:id, :names)
=> [[1, 'Obie']]
>> User.pluck(:name)
=> ['Obie']

Retorna un arreglo de valores de la columna especificada con el tipo de data correspondiente.

###9.3.7 `sum(column_name, *options)

Calcula un valor sumado en la base de datos usando SQL. El primer parámetro puede ser un símbolo identificando la columna que será sumada.

##9.4 Herencia de una sólo tabla (single-table inheritance (STI))

Muchas aplicaciones comienzan con un modelo User de algún tipo. En el tiempo, en la forma que distintos tipos de usuarios emergen, puede hacer sentido hacer una gran distinción entre ellos. Las clases Admin y Guest son introducidas como sub-clases de User. Ahora el comportamiento compartido puede residir en User, y el comportamiento particular puede ser puesto en las sub-clases. Sin embargo, toda la data del usuario puede aún residir en la tabla users -todo lo que usted necesita es introducir una columna type que tendrá el nombre de la clase que debe ser instanciada por una fila determinada.

Para seguir explicando la herencia de tabla única, volvamos a nuestro ejemplo de la clase Timesheet. Necesitamos conocer cuantas billable_hours son exepcionales para un usuario dado. El cálculo puede ser implementado en varias formas, pero en este caso hemos elegido escribir un par de clases y métodos de instancia en la clase Timesheet:

class Timesheet < ActiveRecord::Base
  ...

  def billable_hours_outstanding
    if submitted?
      billable_weeks.map(&:total_hours).sum
    else
      0
    end
  end

  def self.billable_hours_outstanding_for(user)
    user.timesheets.map(&:billable_hours_outstanding).sum
  end
end

No estoy sugiriendo que este sea un buen código. Funciona pero es ineficiente y las condiciones if/else son un poco sospechosas. Sus deficiencias se hacen evidentes una vez que emergen requerimientos de marcar Timesheet como pagado. Esto nos fuerza a modificar el método billable_hours_outstanding de Timesheet una vez más:

def billable_hours_outstanding
  if submitted? && not paid?
    billable_weeks.map(&:total_hours).sum
  else
    0
  end
end

Este ultimo cambio es una clara violación del principio open-closed (http://en.wikipedia.org/wiki/Open/closed_principle), el cual lo insta a crear código que esté abierto a las extensiones pero cerrado para las modificaciones. Sabemos que hemos violado el principio porque hemos forzado cambiar al método billable_hours_outstanding para acomodar el nuevo estado de Timesheet. Aunque esto no parece un gran problema en nuestro ejemplo simple, considere la cantidad de código condicional que terminará en la clase Timesheet una vez que comencemos a implementar funcionalidad como paid_hours y unsubmitted_hours.

Así que cual es la respuesta a esta caótica pregunta del constante cambio condicional? Dado que usted está leyendo la sección del libro acerca herencia de tabla única, es probable que no sea una gran sorpresa que pensemos que una buena respuesta es usar herencia orientada al objeto. Para hacer esto, rompamos nuestra clase Timesheet original en cuatro clases.

class Timesheet < ActiveRecord::Base
  # nonrelevant code ommited

  def self.billable_hours_outstanding_for(user)
    user.timesheets.map(&:billable_hours_outstanding).sum
  end
end

class DraftTimesheet < Timesheet
  def billable_hours_outstanding
    0
  end
end

class SubmittedTimesheet < Timesheet
  def billable_hours_outstanding
    billable_weeks.map(&:total_hours).sum
  end
end

Ahora cuando los requerimientos demandan la habilidad de calcular timesheets de pagos parciales, necesitamos sólo agregar algo de comportamiento a una clase PaidTimesheet. Ninguna desordenada sentencia condicional a la vista!

class PaidTimesheet < Timesheet
  def billable_hours_outstanding
    billable_weeks.map(&:total_hours).sum = paid_hours
  end
end

###9.4.1 Mapeando herencia a la base de datos

El mapeo de la herencia de objetos efectivamente a un base de datos relacional no es uno de esos problemas con definición definitiva. Sólo vamos a hablar de una de las estrategias de mapeo que Rails soporta en forma nativa, la cual es la herencia de tabla única, llamada STI como abreviación.

En STI, usted establece una tabla en la base de datos para contener todos los registros para cualquier objeto en una jerarquía de herencia dada. En STI de Active Record esa tabla es nombrada después de la c lase padre de la cima de la jerarquía. En el ejemplo que hemos considerado, esa tabla será llamada timesheets.

Hey, es como la habíamos llamado antes, correcto? Sí, pero para habilitar STI, tenemos que agregar una columna type que contenga un string representando el tipo de objeto almacenado. La siguiente migración puede setear apropiadamente la base de datos para nuestro ejemplo:

class AddTypeToTimesheet < ActiveRecord::Migration
  def change
    add_column :timesheet, :type, :string
  end
end

No se necesita valor por defecto, Una vez que la columna tipo es agregada a un modelo Active Record, Rails automáticamente se preocupa de llenarla con el valor correcto. Usando la consola podemos ver este comportamiento en acción:

>> d = DraftTimesheet.create
>> d.type
=> 'DraftTimesheet'

Cuando usted intenta encontrar un objeto usando los métodos de consulta de una clase STI base, Rails instanciará automáticamente objetos usando las subclases apropiadas. Esto es especialmente útil en situaciones polimórficas, tales como el ejemplo timesheet que hemos descrito, donde obtenemos todos los registros para un usuario particular y luego llamamos métodos que se comportan diferente dependiendo de la clase de objetos.

>> Timesheet.first
=> #<DraftTimesheet: 0x2212354...>

Nota: Rails no se quejara por las columnas desaparecidas; simplemente las ignorará. Recientemente, el mensaje de error fue reformulado con una mejor explicación, pero muchos desarrolladores separan los mensaje de errores y luego gastan intentando imaginar que está mal con sus modelos. (Mucha gente separa columnas de barras laterales también cuando leen libros, pero, hey, pero al menos yo estoy doblando sus posibilidades de aprender acerca de este problema).

###9.4.2 Consideraciones de STI

Aunque Rails hace muy simple usar la herencia de tabla única, hay cuatro advertencias que usted debe tener en mente.

Primero, usted no puede tener un atributo en dos subclases diferentes con el mismo nombre pero con diferente tipo. Ya que Rails usa una tabla para almacenar todas las subclases, estos atributos con el mismo nombre ocuparán la misma columna en la tabla. Francamente, no hay muchas razones para que esto sea un problema, a menos que usted tome muy malas decisiones de modelamiento.

Segundo y más importante, usted necesita tener una columna por atributo sobre cualquier subclase, y cualquier atributo que no sea compartido por todas las subclases debe aceptar valores nil. En el ejemplo recurrente, PaidTimesheet tiene una columna paid_hours que no es usada por ninguna de las otras subclases. DraftTimesheet y SubmittedTimesheet no usan la columna paid_hours y la dejan nula en la base de datos. Con el fin de validar la data de las columnas no compartidas por todas las subclases, usted debe usar Active Record y no la base de datos.

Tercero, no es buena idea tener subclases con demasiados atributos únicos. Si usted lo hace, usted tendrá una base de datos con muchos valores nulos en ella. Normalmente, un árbol de subclases con una gran cantidad de atributos únicos sugiere que algo está mal con el diseño de su aplicación y que usted debe refactorizar. Si usted tiene una tabla STI que se le está yendo de las manos, es tiempo de reconsiderar su decisión de usar herencia para resolver su problema particular. Quizá su clase base es demasiado abstracta?

Finalmente, las restricciones de una base de datos heredada pueden requerir un diferente nombre en la base de datos para la columna type. En este caso, usted puede setear el nuevo nombre de columna usando el método seteador de clases inheritance_column en la clase base. Para el ejemplo Timesheet podemos hacer lo siguiente:

class Timesheet < ActiveRecord::Base
  self.inheritance_column = 'object_type'
end

Ahora Rails llenará automáticamente la columna object_type con el tipo de objeto.

###9.4.3 STI y las asociaciones

Parece muy común para aplicaciones, particularmente las que manejan data, tener modelos que son muy similares en términos de su costo de caga de datos, la mayoría varía en su comportamiento y en las asociaciones con otras. Si usted usó lenguajes orientados al objeto antes que Rails, usted está probablemente acostumbrado a romper dominios de problemas en estructuras jerárquicas.

Tome, por ejemplo, una aplicación Rails que trabaja con la población de estados, países, ciudades, y barrios. Todos ellos son lugares, lo cual puede llevarlo a definir una clase STI llamada Place como muestra el listado 9.2. También incluimos el schema de la base de datos por claridad (Para información de schemas autogenerada en la cima de su clase modelo, inetente la gema en https://github.com/ctran/annotate_models):

Listado 9.2 El schema de base de datos Places y la clase Pace

# == Schema Information
#
# Table name: places
#
# id :integer(11) not null, primary key
# region_id :integer(11)
# type :string(255)
# name :string(255)
# descrption :string(255)
# latitud :decimal(20, 1)
# longitude :decimal(20, 1)
# population :integer(11)
# created_at :datetime
# updated_at :datetime

class Place < ActiveRecord::Base
end

Place es en escencia una clase abstracta. No debe ser instanciada, pero no hay forma a prueba de tontos para obligar esto en Ruby. (No es gran cosa, esto no es Java) Ahora vamos adelante y definamos subclases concretas de Place:

class State < Place
  has_many :counties, foreign_key: 'region_id'
end

class County < Place
  belongs_to :state, foreign_key: 'region_id'
  has_many :cities, foreign_key: 'region_id'
end

class City < Place
  belongs_to :county, foreign_key: 'region_id'
end

Usted puede estar tentado a agregar una asociación cities a State, sabiendo que has_many :through funciona tanto con asociaciones objetivo has_many como belongs_to. Esto podría hacer que la clase State luzca de la siguiente forma:

class State < Place
  has_many :counties, foreign_key: 'region_id'
  has_many :cities, :through: :counties
end

Esto sería cool si funcionara. Desafortunadamente, en este caso particular, ya que sólo hay una tabla subyacente a la que estamos consultando, no hay forma simple de distinguir entre los diferentes tipos de objetos en la consulta:

Mysql::Error: Not unique table/alias: 'places': SELECT places.* FROM
places INNER JOIN places ON places.region_id = places.id WHERE
((places.region_id = 187912) AND ((places.type = 'County'))) AND 
((places.'type' = 'City'))

Que debemos hacer para que funcione? Bien, lo más realista sería usar llaves externas específicas en lugar de tratar de sobrecargar el significado de region_id para todas las subclases. Para empezar, la tabla places se verá como en el ejemplo del Listado 9.3.

Listado 9.3 El schema de la base de datos Places revisado

# == Schema Information
#
# Table name: places
#
# id :integer(11) not null, primary key
# state_id :integer(11)
# county_id :integer(11)
# region_id :integer(11)
# type :string(255)
# name :string(255)
# descrption :string(255)
# latitud :decimal(20, 1)
# longitude :decimal(20, 1)
# population :integer(11)
# created_at :datetime
# updated_at :datetime

Las subclases serán más simples sin la opción foreign_key sobre la asociación. Además usted puede usar una relación has_many regular desde State a City en lugar de la más complicada has_many :through.

class State < Place
  has_many :counties
  has_many :cities
end

class County < Place
  belongs_to :state
  has_many :cities
end

class City < Place
  belongs_to :county
end

Por supuesto, todas esas columnas nulas en la tabla places no le ganaran ningún amigo con los puristas de bases de dato relacionales. Esto es nada, aunque. Solo un poco después en este capítulo, tomaremos una segunda mirada más profunda sobre las relaciones has_many polimórficas, que harán que los puristas definitivamente lo odien.

##9.5 Clases de modelo Base abstractas

En contraste con la herencia de tabla única, es posible para los modelos de Active Record compartir código común vía herencia y aún ser almacenados en diferentes tablas de bases de datos. De hecho todo desarrollador Rails usa un modelo abstracto en su código lo noten o no: ActiveRecord::Base (http://m.onkey.org/namespaced-models).

La técnica involucra la creación de una clase modelo base abstracto que las subclases persistentes puedan extender. Estoa es realmente una de las técnicas más simples que revisamos en este capítulo. Tomemos la clase Place de la sección previa (referida al Listado 9.3) y revísela para que sea una clase base abstracta en el Listado 9.4. Esto es simple - sólo tenemos que agregar una línea de código:

class Place < ActiveRecord::Base
  self.abstract_class = true
end

Marcar un modelo Active Record como abstracto es esencialmente el opuesto de hacerlo una clase STI con una columna tipo. Usted le esta contando a Rails, "Hey, no deseo que asumas que hay una tabla llamada places".

En su ejemplo corriendo esto significa que deberemos establecer tablas para states, counties, y cities, lo cual puede ser exactamente lo que deseamos. Recuerde, sin embargo, que dejaríamos de ser capaz de consultar a través de subtipos con código como Place.all.

Las clases abstractas están en un área de Rails donde no hay muchas reglas seguras y rápidas para guiarlo -la experiencia y las tripas le ayudaran a salir.

En caso de que no lo haya notado aún, tanto los métodos de clase como los de instancia son compartidos hacia abajo en la jerarquía de los modelos de Active Record. Esto significa que podemos poner todo tipo código dentro de Place que pueda ser útil para sus subclases.

##9.6 Relaciones has_many polimórficas

Rails le da la habilidad de hacer que una clase pertenezca belongs_to a más de un tipo de otras clases, como elocuentemente estableción el blogger Mike Bayer:

La "asociación polimórfica", por otro lado, si bien tiene cierta semejanza con la unión polimórfica regular de una jerarquía de clases, no es realmente lo mismo, ya que sólo está tratando con una asociación particular a una sola clase de destino desde cualquier número de clases de fuentes, clases de fuentes que no tienen nada más que ver las unas con las otras; por ejemplo, ellas no están en ninguna relación de herencia particular y probablemente son almacenadas en tablas completamente diferentes. De esta forma, la asociación polimórfica tiene mucho menos que hacer con la herencia de objetos y mucho más que hacer con la programación orientada a aspectos (AOP); un concepto particular necesita ser aplicado a un conjunto divergente de entidades las cuales de otra forma no estrían directamente relacionadas. Tal concepto se conoce como una preocupación de corte transversal, tales como, todas las entidades en su dominio necesitan para soportar un registro de la historia de todos los cambios una tabla de registro común. En el ejemplo AR, un objeto Order y un objeto User se ilustra que requieren linquear a un objeto Address (http://techspot.zzzeek.org/2007/05/29/polymorphic-associations-with-sqlalchemy/).

En otras palabras, esto no es polimorfismo en el sentido de la orientación al objeto típico de la palabra; si no que es algo típico de Rails.

###9.6.1 En el caso de modelos con comentarios

En nuestro ejemplo recurrente de tiempo y gastos, asumamos que deseamos que tanto BillableWeek como Timesheet tengan muchos comentarios (y compartan la clase Comment). Una forma simplista de resolver este problema puede ser que la clase Comment pertenezca tanto a BillableWeek como a Timesheet y tenga a billable_week_id y a timesheet_id como columnas en la tabla de la base de datos.

class Comment < ActiveRecord::Base
  belongs_to :timesheet
  belongs_to :expense_report
end

Llamo esta aproximación simplista porque puede ser difícil de trabajar y extender. Entre otras cosas, usted podría necesitar agregar código a la aplicación para asegurarse que un Comment nunca pertenezca tanto a BillableWeek como a Timesheet al mismo tiempo. El código para descifrar a que un comentario dado está atachado puede ser incómodo de escribir. Incluso peor, cada vez que usted quiera estar habilitado para agregar comentarios a otro tipo de clase, usted tendrá que agregar otra columna llave externa anulable a la tabla de comentarios.

Rails resuelve este problema en una forma elegante permitiéndonos definir que es una asociación polimórfica, lo cual fue cubierto cuando describimos la opción polymorphic: true de la asociación belongs_to en el Capítulo 7, "Asociaciones de Active Record".

9.6.1.1 La interface

Usando una relación polmórfica, necesitamos definir sólo un único belongs_to y agregar un par de columnas relacionadas a la tabla de base de datos subyacente. A partir de este momento, cualquier clase en nuestro sistema puede tener comentarios atachados a ella (lo cual puede hacerla comentable) sin la necesidad de modificar el schema de base de datos o el modelo Comment mismo.

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

No hay una clase Commentable (o modulo) en su aplicación. Nombramos la asociación commentable porque describe precisamente la interface de objetos que asociaremos de esta forma. El nombre commentable retornará de nuevo sobre el otro lado de la asociación:

class Timesheet < ActiveRecord::Base
  has_many :comments, as: commentable
end

class BillableWeek < ActiveRecord::Base
  has_many :comments, as: :comentable
end

Aquí tenemos la amigable asociación has_many usando la opción :as. La opción :as marca esta asociación como polimórfica y espcifica cual interface estamos usando sobre el otro lado de la asociación. Mientras entamos sobre el tema, el otro final de un belongs_to polimórfico puede ser un has_many o un has_one y trabajan idénticamente.

9.6.1.2 Las columnas de la base de datos

Aquí está una migración que puede crear la tabla comments:

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.text :body
      t.integer :commentable
      t.string :commentable_type
    end
  end
end

Como usted puede ver, hay una columna llamada commentable_type, la cual almacena el nombre de clase de los objetos asociados. La API de migraciones realmente le da una abreviación de una línea con el método references, el cual toma una opción polymorphic:

create_table :comments do |t|
  t.text :body
  t.references :commentable, polymorphic: true
end

Podemos ver como funciona usando la consola de Rails (algunas líneas son omitidas para abreviar):

>> c = Comment.create(body: 'I cuold be commenting anything.')
>> t = Timesheet.create
>> b = BillableWeek.create
>> c.update_attribute(:commentable, t)
=> true
>> "#{c.commentable_type}: #{c.commentable_id}"
=> "Timesheet: 1"
>> c.update_attribute(:commentable, b)
=> true
>> "#{c.commentable_type}: #{c.commentable_id}"
=> "BillableWeek: 1"

Como usted puede ver, tanto el Timesheet como el BillableWeek que usamos en la consola tiene el mismo id (1). Gracias al atributo commentable_trype almacenado en un string, Rails puede imaginar cual es el objeto relacionado correcto.

9.6.1.3 has_many :through y los polimórficos

Hay algunas limitaciones lógicas que entran al juego con las asociaciones polimórficas. Por ejemplo, ya que s imposible para Rails conocer las tablas necesarias para juntar (join) a través de asociaciones polimórficas, el siguiente código hipotético, el cual intenta encontrar todo lo que el usuario ha comentado, no funcionará:

class Comment < ActiveRecord::Base
  belongs_to :user # author of the comment
  belongs_to :commentable, polymorphic: true
end

class User < ActiveRecord::Base
  has_many :comments
  has_many :commmentables, through: :comments
end

>> User.first.commentables
ActiveRecord::HasManyThroughAssociationPolymorphicSourceError: Cannot 
have a has_many :through association 'User#commentables' on the polymorphic object

Si usted realmente lo necesita, has_many :through es posible con asociaciones polimórficas pero sólo especificando exactamente que tipo de asociación polimórfica usted desea. Para hacerlo, usted debe usar la opción :source_type. En la mayoría de los casos, usted también necesitará usar la opción :source, ya que el nombre de la asociación puede no coincidir con el nombre de la interface usado para la asociación polimórfica:

class User < ActiveRecord::Base
  has_many :comments
  has_many :commented_timesheets, through: :comments, source: :commentable, source_type: 'Timesheet'
  has_many :commented_billable_weeks, through: :comments, source: :commentable, source_type: 'BillableWeek'
end

Es extenso, y el esquema completo pierde su elegancia si usted toma este camino, pero funciona:

>> User.first.commented_timesheets.to_a
=> [#<Timesheet ...>]

##9.7 Enums

Una de las nuevas adiciones a Active Record introducida en Rails 4.1 es la habilidad de setear un atributo como enumerable. Una vez que un atributo es seteado como enumerable, Active Record restringirá la asignación del atributo a la colección de valores pre-definidos.

Para declarar un atributo enumerable, use el método de clase estilo macro enum, pásele el nombre de un atributo y una arreglo de valores de estado que el atributo puede setear.

class Post < ActiveRecord::Base
  enum status: %i(draft published archived)
  ...
end

Active Record implícitamente mapea cada valor predefinido de un atributo enum a un entero; de esta forma, el tipo de columna del atributo enum debe ser un entero también. Por defecto, un atributo enum será seteado a nil. Para setear un estado inicial, uno puede setear un valor por defecto en la migración. Es recomendado setear este valor al primer estatus declarado, el cual se debe mapear a 0.

class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.integer :status, default: 0
    end
  end
end

Por ejemplo, dado nuestro ejemplo, el estado por defecto de un modelo Post es "draft":

>> Post.new.status
=> "draft"

Usted podría nunca tener que trabajar con el tipo de dato entero subyacente de un atributo enum, ya que Active Record crea los métodos predicado y bang para cada valor de estatus.

post.draft!
post.draft?  # => true
post.published  # => false
post.status # => "draft"

post.published!
post.published?  # => true
post.draft?  # => false
post.status  # => "published"

post.status = nil
post.status.nil?  # => true
post.status  # => nil

Active Record también provee métodos scope para cada valor status. Invocando uno de estos scopes retornarán los registros con ese estado.

Post.draft
# Post Load (0.1ms)  SELECT "post".* FROM "post"
  WHERE "posts"."status" = 0

Nota: Active Record crea un método de clase con un nombre pluralizado del enum definido sobre el modelo que retorna un hash con la llave y valor de cada status. En nuestro ejemplo precedente, el método Post podría tener un método de clase llamado statuses.

>> Post.statuses
=> {"draft"=>0, "published"=>1, "archived"=>2}

Usted puede sólo necesitar accesar este método de clase cuando necesite conocer el valor ordinal subyacente de un enum.

Con la adición del atributo enum, Active Record finalmente tiene una simple máquina de estado de alto nivel. Esta característica sola puede simplificar modelos que tienen dependencia previa de múltiples campos booleanos para manejar estado. Si usted requiere una funcionalidad más avanzada, como callbacks de transición de estados y transiciones condicionales, se recomienda usar una máquina de estado completa como state_machine (https://github.com/pluginaweek/state_machine).

##9.8 Restricciones de llave externa

Como trabajaremos hacia el final de la cobertura de Active Record de este libro, usted habrá notado que no hemos tocado un tema de importancia particular para muchos programadores: restricciones de llaves externas en una base de datos. Esto es principalmente porque el uso de restricciones de llaves externas simplemente no es la forma de Rails de enfrentar el problema de la integridad relacional. Por decirlo suavemente, esa opinión es controversial, y algunos desarrolladores le han escrito a Rails y a sus autores para expresarlo.

No hay realmente nada que le detenga de agregar restricciones de llaves externas a las tablas de su base de datos, aunque haría bien en esperar hasta que la mayoría del desarrollo fuera hecho. La excepción, por supuesto, son estas asociaciones polimórficas, las cuales las cuales son probablemente la más extrema manifestación de la opinión de Rails en contra de las restricciones de llaves externas. A menos que usted esté armado para la batalla, usted puede no desear hablar este tema particular con su DBA. ##9.9 Módulos para reusar comportamiento común

En esta sección, hablaremos acerca de una estrategia para romper la funcionalidad que es compartida entre distintas clases de modelos. En lugar de usar la herencia, ponemos el código compartido dentro de módulos.

En la sección "Relaciones polimórficas has_many" en este capítulo, describimos como agregar una característica de comentarios a nuestro ejemplo recurrente de la aplicación "Tiempo y gastos". Continuaremos dándole contenido a este ejemplo, ya que se presta para ser factorizado en módulos.

Los requerimientos que implementaremos son los siguientes: Tanto usuarios como aprobadores deben estar habilitados a agregar sus comentarios a Timesheet como a ExpenseReport. También, ya que los comentarios son indicadores de que un timesheet o expense report requiere escrutinio extra o tiempo de proceso, los administradores de la aplicación estarán habilitados para ver fácilmente una lista de comentarios recientes. La naturaleza humana siendo lo que es, los administradores ocasionalmente pasarán por alto los comentarios sin llegar a leerlos, así que el requerimiento especifica que un mecanismo debe ser provisto para marcar comentarios como "OK" primero por el aprobador y luego por el administrador.

Una vez más, aquí está el polimórfico has_many :comments, as: :commentable que usamos como el fundamento de esta funcionalidad:

class Timesheet < ActiveRecord::Base
  has_many :comments, as: :commentable
end

class ExpenseReport < ActiveRecord::Base
  has_many :comments, as: commentable
end

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Luego habilitamos el controlador y la acción que lista los recientes 10 comentarios con links a los items que ellos están atachados.

class Comment < ActiveRecord::Base
  scope :recent, -> { order('created_at desc').limit(10) }
end

class CommentsController < ApplicationController
  before_action :requiere_admin, only: :recent
  expose(:recent_comments) { Comments.recent }
end

Aquí algunas de los templates de vista simples usado para desplegar los comentarios recientes:

%ul.recent.comments
  - recent_comments.each do |comment|
    %li.comment
      %h4= comment.created_at
      = comment.text
      .meta
        Comment on:
        = link_to comment.commentable.title, coment.commentable
        # Yes, this would result in N+1 selects.

Hasta ahora todo bien. Las asociaciones polimórficas simplifican el acceso a todo tipo de comentarios en un listado. Con el fin de encontrar todos los comentarios sin revisar para un ítem, podemos usar scope nombrado sobre la clase Comment junto a la asociación comments.

class Comment < ActiveRecord::Base
  scope :unreviewed, -> { where(reviewed: false) }
end

>> timesheet.coments.unreviewed

Tanto Timesheet como ExpenseReport actualmente tienen métodos idénticos has_many para comments. Escencialmente, ambos comparten una interface común. Ellos son comentables!

Para minimizar la duplicación, podemos especificar interfaces comunes que compartan código en Ruby incluyendo un módulo en cada una de esas clases, donde el módulo contiene el código común a todas la implementaciones de la interface común. Así, principalmente a modo de ejemplo, vamos adelante a definir un módulo Commentable que haga sólo esto e inlcluirlo en nuestra clase de modelo:

module Commentable
  has_many :comments, as: :commentable
end

Class Timesheet < ActiveRecord::Base
  include Commentable
end

class ExpenseReport < ActiveRecord::Base
  include Commentable
end

Whoops, este código no funciona! Para repararlo, necesitamos comprender un aspecto esencial de la forma en que Ruby interpreta nuestro código lideando con clases abiertas.

###9.9.1 Una revisión de scopes de clases y los contextos

En muchos otros lenguajes de programación orientados al objeto interpretados, usted tiene dos fases de ejecución: una en la cual el interprete carga las definiciónes de clase y dice, "Esta es la definición de qué debo trabajar con" y una segunda en la cual ejecuta el código. Esto hace difícil (aunque no necesariamente imposible) agregar nuevos métodos a la clase dinámicamente durante la ejecución.

En contraste, Ruby le permite agregar métodos a una clase en cualquier momento. En Ruby, cuando usted tipea class MyClass, usted está haciendo más que simplemente contarle al interprete que defina una clase; usted le está contando que "ejecute el siguiente código en el scope de esta clase."

Digamos que usted tiene el siguiente script:

class Foo < ActiveRecord::Base
  has_many :bars
end

class Foo < ActiveRecord::Base
  belongs_to :spam
end

Cuando el intérprete toma la línea1, le estamos contando que ejecute el código siguiente (hasta que encuentre el end) en el contexto del objeto clase Foo. Ya que la clase Foo no existe aún, sigue adelante y crea la clase. En la línea 2, ejecutamos la línea has_many :bars en el contexto del objeto de clase Foo. Lo que sea que el método has_many hace, lo hace ahora.

Cuando decimos nuevamente class Foo en la línea 4, estamos una vez más contándole al interprete que ejecute el código siguiente en el contexto del objeto clase Foo, pero esta vez el interprete ya conoce la clase Foo; por lo que no crea una nueva clase. Por lo tanto, en la línea 5, sólo le estamos contando al intérprete que ejecute la instrucción belongs_to en el contexto del objeto clase Foo

Con el fin de ejecutar las instrucciones has_many y belongs_to, estos métodos necesitan existir en el contexto en el que ellos son ejecutados. Ya que están definidos como métodos de clase en ActiveRecord::Base, y hemos definido previamente la clase Foo como una extensión de ActiveRecord::Base, el código se ejecutará sin problema.

Sin embargo, digamos que definimos nuestro módulo Commentable de la siguiente forma:

module Commentable
  has_many :coments, as: :commentable
end

En este caso, obtenemos un error cuando intentamos ejecutar la instrucción has_many. Esto es porque el método has_many no está definido en el contexto de objeto módulo Commentable

Dado que ahora sabemos cómo Ruby está interpretando el código, ahora concluimos lo que realmente deseamos es que la instrucción has_many sea ejecutada en el contexto de la clase que la incluye.

###9.9.2 El callback included

Afortunadamente, la clase Module de Ruby define un callback que podemos usar para hacer exáctamente esto. Si un objeto Module define un método inlcuded, este corre donde quiera que el módulo es incluido en otro módulo o clase. El argumento pasado a este método es el método/clase dentro del cual este módulo está siendo incluido.

Podemos definir un método included sobre nuestro objeto módulo Commentable para que se ejecute la instrucción has_many en el contexto de la clase que lo inlcuye (Timesheet, ExpenseReport, etc.):

module Commentable
  def self.included(base)
    base.class_eval do
      has_many :comments, as: :comentable
    end
  end
end

Ahora cuando incluimos el módulo Commentable en nuestras clases de módelo, se ejecutará la instrucción has_many cómo si las hubiéramos tipeado dentro del cuerpo de cada una de esas clases.

La técnica es suficientemente común, en Rails y las gemas, esto fue agregado como un concepto de primera clase en la API Active Suport desde Rails 3. El ejemplo previo se hace más corto y fácil de leer como un resultado:

# app/models/concern/commentable.rb
module Commentable
  extend ActiveSupport::Concern
  included do
    has_many :comments, as: :commentable
  end
end

Lo que sea que esté dentro del bloque included será ejecutado en el contexto de la clase donde el módulo es incluido.

Desde la versión 4.0, Rails inlcuye el directorio app/models/concerns como un lugar para poner todos las preocupaciones (concerns) de su aplicación. Cualquier archivo encontrado dentro este directorio será automáticamente parte de la ruta de carga de la aplicación.

Hay un delicado equilibrio que golpea aquí. la magia como include Commentable ciertamente nos ahorra escritura y hace nuestro modelo menos complejo, pero esto puede también significar que su código de asociación esté haciendo cosas que usted no conoce. Esto puede conducir a confuciones y horas de romperse la cabeza mientras usted sigue el código en un módelo separado. Mi preferencia personal es dejar todas las asociaciones en el modelo y extenderlas con un módulo. De esta forma usted puede rápidamente tener una lista de todas sus asociaciones con sólo mirar el código.

##9.10 Modificar clases Active Record en tiempo de ejecución

Las capacidades de metaprogramación de Ruby, combinada con el callback after_find, abren la puerta al agunas posibilidades interesantes, especialmente si usted está dispuesto a distorsionar su percepción de la diferencia entre código y data. Estoy hablando de modificar el comportamiento de las clases de modelo en tiempo de ejecución, como si ellas estuvieran cargadas en su aplicación.

El listado 9.5 es un ejemplo de la técnica drásticamente simplificada, el cual asume la presencia de una columna config en su modelo. Durante el callback after_find, obtenemos un identificador para la clase singleton única de la instancia de modelo que está siendo cargada (No espero que esto le haga sentido a menos que le sea familiar las clases singleton de Ruby y tenga la habilidad de evaluar strings de Ruby arbitrarios en tiempo de ejecución. Un buen lugar para comenzar es http:://yehudakatz.com/2009/11/15/metaprograming-in-ruby-its-all-about-the-self/). Luego ejecutamos el contenido del atributo config perteneciente a esta instancia particular de Account, usando el método class_eval de Ruby. Ya que estamos haciendo esto usando la clase síngleton para esta instancia en lugar de una clase Account global, otras instancias account en el sistema están completamente inafectadas.

Listado 9.5 Metaprogramación en tiempo de ejecución con after_eval

class Account < ActiveRecord::Base
  ...

  protected

  def after_find
    singleton = class << self; self; end
    singleton.class_eval(config)
  end

Use técnicas poderosas como esta en una aplicación de cadena de suministro que escribí para un gran cliente industrial. Un lote es un término genérico en la industria para describir un envío de productos. Dependiendo del vendedor y producto involucrado, los atributos y lógica de negocios para un lote dado varía mucho. Ya que el conjunto de vendedores y productos manejados cambió en torno a una semana (en ocasiones diariamente), el sistema necesita ser reconfigurable sin requerir un desarrollo de producción.

Sin entrar en muchos detalles, la aplicación permite al programador de mantenimiento fácilmente personalizar el comportamiento del sistema manipulando código Ruby almacenado en la base de datos, asociado con cualquier producto que contenga el lote.

Por ejemplo, una de las reglas de negocio asociadas con el envío de mantequilla por Acme Dairy Co. puede obligar a un código de producto integral estricto, exactamente 10 dígitos de largo. El código (almacenado en la base de datos) asociado con la entrada de producto para matequilla de Acme Dairy debería contener las siguientes dos líneas:

validates_numericality_of :product_code, only_integer: true
validates_length_of :product_code, is: 10

###9.10.1 Consideraciones

Una descripción relativamente completa de todo lo que usted puede hacer con la metaprogramación de Ruby, y cómo hacerlo correctamente, podría llenar su propio libro. Por ejemplo, usted puede imaginar que hacer cosas com ejecutar código Ruby arbitrario directamente desde su base de datos puede ser inherentemente peligroso. Eso es porque yo enfatizo que los ejemplos mostrados aquí son muy simplificados. Todo lo que deseo es darle una impresión de las posibilidades.

Si usted ha decidido apoyarse en este tipo de técnicas en las aplicaciones del mundo real, usted tendrá que considerar la seguridad y un flujo de trabajo aprobado y una cantidad de otras preocupaciones importantes. En lugar de permitir que código Ruby arbitrario sea ejecutado, usted puede sentirse motivado a limitarlo a un pequeño subconjunto relacionado con el problema que maneja. Usted puede diseñar una API compacta o incluso ahondar en la autoría de un lenguaje de dominio específico (DSL), diseñado para específicamente expresar las reglas de negocio y comportamientos que den ser cargados dinámicamente. Bajando a la cueva del conejo, usted puede escribir analizadores personalizados para su DSL que se pueden ejecutar en diferentes contextos -algunos para la detección de errores y otros para la creación de reportes. Esta es una de esas áreas donde las posibilidades son ilimitadas.

###9.10.2 Ruby y los lenguajes de dominio específico

Mi colega formador Jay Fields y yo fuimos pioneros en la mezcla de la metaprogramación de Ruby, y los lenguajes de dominio específico internos mientras desarrollabamos aplicaciones Rails para clientes. Yo aún hablo ocasionalmente en conferencias y blogueo acerca de escribir DSLs en Ruby.

Jay ha escrito también y realizado conferencias acerca de su evolución en las técnicas DSL de Ruby, las cuales ha llamado lenguaje natural de negocios (o BNL abreviado en http://blog.jayfield.com/2006/07/business-natural-languages-material.html) . Cuando desarrolla BNLs, usted diseña un lenguaje de dominio específico que no necesariamente es una sintaxis de Ruby válida pero está lo suficientemente cerca de ser transformada fácilmente a Ruby y ejecutada en tiempo de ejecución, como se muestra en el Listado 9.6.

Listado 9.6 Ejemplo de Business Natural Language

employee John Doe
compensate 500 dollars for each deal closed in the past 30 days
compensate 100 dollars for each active deal that closed more than 365 days ago
compensate 5 percent of gross profits if gross profits are greater than 1,000,000 dollars
compensate 3 percent of gross profits if gross profits are grater than 2,000,000 dollars
compensate 1 percent of gross profits if gross profits are grater than 3,000,000 dollars

La habilidad de potenciar técnicas avanzadas tales como DSLs son aún otra poderosa herramienta en manos de un desarrollador Rails experimentado.

Las DSLs apestan! Excepto las escritas por Obie, por supuesto. Los únicos que pueden leer y escribir la mayoría de las DSLs son sus autores originales. En la medida que un desarrollador se hace cargo de un proyecto, es más rápido volver a implementar que comprender la particularidad y que palabras está habilitado para usar en un DSL existente. De hecho, una gran cantidad de la metaprogramación de Ruby apesta también. Es común en las personas dotadas de estas nuevas herramienta irse un poco por la borda. Yo considero la metaprogramación, self_included, class_eval y sus amigos tiene poco olor a código en la mayoría de los proyectos. Si usted está haciendo una aplicación web, los futuros desarrolladores y mantenedores del proyecto apreciarán que usted use métodos simples, granulares y bien probados más que parches de mono dentro de las clases existentes u ocultar asociaciones dentro de módulos. Es decir, si usted puede evitarlos, su código será más poderoso de lo que pueda imaginar.

##9.11 Usando objetos valor

En el diseño dirigido por dominios (DDD http://domaindrivendesign.org), hay una distinción entre objetos de entidad y objetos de valor. Todos los objetos de modelo que heredan de ActiveRecord::Base pueden ser considerados objetos de entidad en DDD. Un objeto de entidad se preocupa de la identidad, ya que cada uno es único. En Active Record, la unicidad es derivada de una llave primaria.

Al comparar dos diferentes objetos entidad buscando la igualdad, siempre retornará falso, incluso si todos sus atributos (salvo la llave primaria) son equivalentes.

Aquí un ejemplo comparando dos direcciones Active Record:

>> home = Adress.create(city: "Broklyn", state: "NY")
>> office = Address.create(city: "Broklyn", state: "NY")
>> home == office
=> false

En este caso, usted está creando realmente dos nuevos registros Address y almacenándolos en la base de datos; sin embargo, ellos tienen diferente valor de la llave primaria.

Los objetos valor, por el otro lado, sólo se preocupan de que sus atributos sean iguales. Cuando creamos objetos valor para usar con Active Record, usted no hereda de Active Record::Base, sino que a cambio, simplemente define un objeto Ruby estándar. Esto es una forma de composición llamada un agregado en DDD. Los atributos del objeto valor son almacenados en la base de datos junto con los objetos padres, y el objeto Ruby estándar provee un tipo de interacción con esos valores en una forma más orientada al objeto.

Un ejemplo simple es una Person con una única Address. Para modelar esto usando composición, primero necesitamos un modelo Person con campos para las Address. Lo creamos con la siguiente migración:

class CreatePeople < ActiveRecord::Migration
  def change
    create_table :people do |t|
      t.string :name
      t.string :address_city
      t.string :address_state
    end
  end
end

El modelo Person lucirá así:

class Person < ActiveRecord::Base
  def address
    @address ||= Address.new(address_city, address_state)
  end

  def address=(address)
    self[:address_city] = address.city
    self[:address_state] = address.state

    @address = address
  end
end

Necesitamos un objeto Address correspondiente, el cual luce así:

class Address
  attr_reader :city, :state

  def initialize(city, state)
    @city, @state = city, state
  end

  def == (other_address)
    city == other_address.city && state == other_address.state
  end
end

Note que este es sólo un objeto Ruby estándar que no hereda de ActiveRecord::Base. Hemos definido métodos reader para nuestros atributos y son asignados sobre la inicialización. También tenemos que definir nuestro método == para usar en las comparaciones. Envolviendo todo esto, tenemos el siguiente uso:

>> gary = Person.create(name: "Gary")
>> gary.address_city = "Brooklyn"
>> gary.address_state = "NY"
>> gary.address
=> #<Address:0x007fcbfcce0188 @city="Brooklyn", @state="NY">

Alternativamente, usted puede instanciar la dirección directamente y asignarla usando el accesor address:

>> gary.address = Address.new("Brooklyn", "NY")
>> gary.address
=> #<Address:0x007fcbfcce0188 @city="Brooklyn", @state="NY">

###9.11.1 Inmutabilidad

Es también importante tratar el objeto valor como inmutable. No habilitarlos para ser cambiados después de la creación. A cambio, se debe crear una nueva instancia de objeto con el nuevo valor a cambio. Active Record no almacenará objetos valor que hayan sido cambiados por medios distintos que el método writer en el objeto padre.

9.11.1.1 La gema Money

Una aproximación común al uso de objetos valor esta en conjunto con la gema money (https://github.com/RubyMoney/money).

class Expense < ActiveRecord::Base
  def cost
    @cost ||= Money.new(cents || 0, currency || Money.default_currency)
  end

  def cost=(cost)
    self[:cents] = cost.cents
    self[:currency] = cost.currency.to_s

    cost
  end

Recuerde agregar una migración con las dos columnas -el entero cents y el string currency que el dinero necesita.

class CreateExpenses < ActiveRecord::Migration
  def change
    create_table :expenses do |t|
      t.integer :cents
      t.string :currency
    end
  end
end

Ahora cuando preguntamos por el seteo del costo de un ítem, usaremos una instancia de Money.

>> expense = Expense.create(cost: Money.new(1000, "USD"))
>> cost = expense.cost
>> cost.cents
=> 1000
>> expense.currency
=> "USD"

##9.12 Modelos no persistentes

En Rails 3, si uno busca usar objetos Ruby estándar con los helpers de Action View, tales como form_for, el objeto debe actuar como una instancia Active Record. Esto involucro la inclusión/extensión de varios mixins de módulos de Active Record e implementar el método persisted?. Como mínimo, ActiveModel::Conversion debe ser incluido y ActiveModel::Naming extendido. Estos dos módulos por sí solos proveen al objeto todos los métodos necesitados por Rails para determinar path, rutas y nombres parciales. Opcionalmente, extendiendo ActiveRecord::Translation agrega soporte de internacionalización a sus objetos, mientras que incluir ActiveRecord::Validations le permite definir validaciones. Todos los módulos son cubiertos en la referencia API de Active Model.

Para ilustrar, asumamos que tenemos una clase Contact que tiene atributos para name, email y message. La siguiente implementación es Action Pack y Action View compatible tanto con Rails 3 como con Rails 4:

class Contact
  extend ActiveModel::Naming
  extend ActiveModel::Translation
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attr_accessor :name, :email, :message

  validates :name, presence: true
  validates :email
    format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2, })\z/ }, presence: true
  validates :message, length: {maximum: 1000}, presence: true

  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end

  def persisted?
    false
  end
end

Nuevo en Rails 4 es el ActiveModel::Model, un módulo mixin que remueve el trabajo penoso de manualmente tener que implementar una interface compatible. Hay que cuidar de incluir/extender los módulos mencionados antes, definir un initilizer para setear todos los atributos en la inicialización, y setear persisted? a false por defecto. Usando ActiveModel::Model, la clase Contact puede ser implementada como sigue:

class Contact
  include ActiveModel::Model

  attr_accessor :name, :email, :message

  validates :name, presence: true
  validates :email, format { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2, })\z/ }, presence: true
  validates :message, length: {maximum: 1000}, presence: true
end

##9.13 Mejoras de PostgreSQL

Fuera de todas las bases de datos soportadas en Active Record, PostgreSQL recibió la mayor atención durante el desarrollo de Rails 4. En esta sección, veremos varias adaptaciones hechas al adaptador de la base de datos PostgreSQL.

###9.13.1 Data sin schema con hstore

El tipo de datos hstore de PostgreSQL permite el almacenamiento de pares llave/valor o simplemente un hash dentro de una columna única. En otras palabras, si usted está usando PostgreSQL y Rails 4, usted puede ahora tener data sin esquema dentro de sus modelos.

Para comenzar, primero setee su base de datos PostgreSQL para usar extensiones hstore vía el método de migración enable_extension:

class AddHstoreExtension < ActiveRecord::Migration
  def change
    enable_extension "hstore"
  end
end

Luego, agegue el tipo de columna hstore al modelo. Para el propósito de nuestros ejemplos, usaremos el modelo Photo con una tributo hstore properties.

class AddPropertiesToPhoto < ActiveRecord::Migration
  change_table :photos do |t|
    t.hstore :properties
  end
end 

Con la columna hstore properties seteada, estamos habilitados para escribir un hash en la base de datos:

photo = Photo.new
photo.properties # nil
photo.properties = { aperture: 'f/4.5', shutter_speed: '1/100 secs' }
photo.save && photo.reload
photo.properties # { aperture: 'f/4.5', shutter_speed: '1/100 secs' }

Aunque esto funciona suficientemente bien, Active Record no lleva seguimiento de ningún cambio realizado en el atributo properties mismo.

photo,properties[:taken] = Time.current
photo.properties
# { aperture: 'f/4.5', shutter_speed: '1/100 secs',
# :taken=>Wed, 23 Oct 2013 16:03:35 UTC +00:00}

photo.save && photo.reload
photo.properties # { aperture: 'f/4.5', shutter_speed: '1/100 secs' }

Como con otros tipos de columnas PostgreSQL, como el array y json, usted debe contarle a Active Record que un cambio ha tenido lugar vía el método <attribute_will_change!. Sin embargo, una mejor solución es usar el método estilo macro store_accessor para agregar accessors read/write a los valores hstore.

class Photo < ActiveRecord::Base
  store_accessor :properties, :aperture, :shutter:speed
end

Cuando seteamos nuevos valores a cualquiera de esos accessors, Active Record está habilitado a dar seguimiento a los cambios realizados en el hash subyacente, eliminando la necesidad de llamar al método <atribute>_will_change!. Como todo accessor, pueden tener validaciones de Active Record agregadas a ellos y también pueden ser usados en formularios.

photo = Photo.new
photo.aperture = "f4/5"
photo.shutter_speed = "1/100 secs"
photo.properties # { aperture: 'f/4.5', shutter_speed: '1/100 secs' }

photo.save && photo.reload

photo.properties # { aperture: 'f/4.5', shutter_speed: '1/100 secs' }
photo.aperture = "f/1.4"

photo.save && photo.reload
photo.properties # { aperture: 'f/1.4', shutter_speed: '1/100 secs' }

Tenga cuidado que cuando un atributo hstore es retornado desde PostgreSQL, todos los llave/valor serán string.

9.13.1.1 Consultando hstore

Para consultar sobre un valor hstore en Active Record, use condiciones string de SQL con el método de consulta where. En aras de la claridad, aquí hay un par de ejemplos de varias consultas que pueden ser hechas sobre una columna del tipo hstore:

# Nonindexed query to find all photos that have a key 'aperture' with a
# value of f/1.4
Photo.where("properties -> :key = :value", key: 'aperture', value: 'f/1.4')

# Indexed query to find all photos that have a key 'aperture' with a value
# of f/1.4
Photo.where("properties @> 'aperture=>f/1,4'")

# All photos that have a key 'aperture' in properties
Photo.where("properties ? :key", key: 'aperture')

# All photos that do not have a key 'aperture' in properties
Photo.where("not properties ? :key", key: 'aperture')

# All photos that contain all keys 'aperture' or 'shutter_speed'
Photo.where("properties ?& ARRAY[:key]", keys: %w(aperture shutter_speed))

# All photos that contain any of the keys 'aperture' or 'shutter_speed'
Photo.where("properties ?| ARRAY[:key]", keys: %w(aperture shutter_speed))

Para mas información sobre cómo construir consultas hstore, usted puede consultar la documentación de PostgreSQL directamente (http://www.postgresql.org/docs/9.3/static/hstore.html).

9.13.1.2 Índices GiST y GIN

Si usted está haciendo cualquier consulta sobre un tipo de columna hstore, asegúrese de agregar al índice apropiado. Cuando agregamos un índice, usted debe decidir si usar un tipo de índice GIN o GiST. El factor que distingue entre los dos tipos de índice es que el índice GIN encuentra tres veces más rápido que el índice GiST; sin embargo, ellos demoran tres veces más tiempo para ser construidos.

Usted puede definir entre los índices GIN o GiST usando las migraciones de Active Record para setear la opció de índice :using a gin o gist respectivamente.

add_index :photos, :properties, using: :gin
# or
add_index :photos, :properties, using: :gist

Los índices GIN y GiST soportan consultas con los operadores @>, ?, ?& y ?|.

###9.13.2 Tipo Array

Otro tipo de columna del tipo NoSQL soportada por PostgreSQL y Rails 4 es array. Esto nos permite almacenar una colección de un tipo de data, como string, dentro de la base de datos misma. Por ejemplo, asumamos que tenemos un modelo Article, podemos almacenar todas las etiquetas del artículo en un atributo array llamado tags. Ya que las etiquetas no son almacenadas en otra tabla, cuando Active Record recupera un artículo de la base de datos, lo hace en una única consulta.

Para declarar una columna como un arreglo, pase true a la opción :array para un tipo de columna como string:

class AddTagsToArticles < ActiveRecord::Migration
  def change
    change_table :articles do |t|
      t.string :tags, array: true
    end
  end
end
# ALTER TABLE "articles" ADD COLUMN "tags" character varying(255)[]

El tipo de columna array también puede aceptar la opción :length para limitar la cantidad de item permitidos en el array.

t.string :tags, array: true, length: 10

Para setear un valor por defecto a una columna array, usted debe usar la notación array de PostgreSQL ({value}). Seteando el valor por defecto a {} segura que cada fila en la base de datos por defecto tendrá un arreglo vacío.

t.string :tags, array: true, default: '{rails, ruby}'

La migración en el ejemplo de código anterior creará un arreglo de strings que por defecto cada fila en la base de datos tendrá un arreglo que contiene los strings "rails" y "ruby".

>> article = Article.create
    (0.1ms) BEGIN
    SQL (66.2ms) INSERT INTO "articles" ("created_at", "updated_at") VALUES
    ($1, $2) RETURNING "id" [["created_at", Wed, 23 Oct 2013 15:03:12

>> article.tags
=> ["rails", "ruby"]

Note que Active Record no da seguimiento a los cambios destructivos o en el lugar a instancias de Array.

article.tags.pop
article.tags # ["rails"]
article.save && article.reload
article.tags # ["rails", "ruby"]

Para asegurarnos de que los cambios sean persistentes, debe contarle a Active Record que el atributo ha cambiado llamando a <attribute>_will_change!.

article.tags.pop
article.tags # ["rails"]
article.tags_will_change!
article.save && article.reload
article.tags # ["rails"]

Si la gema pg_array_parser es incluida en el Gemfile de la aplicación, Rails la usará cuando parsee la representación array de PostgreSQL. La gema incluye una extensión nativa en C y soporte de JRuby.

9.13.2.1 Buscando en Arrays

Si usted desea consultar sobre una columna array usando Active Record, usted debe usar los metodos de PSQL ANY y ALL. Para mostrarlo, dado nuestro ejemplo previo, usando el método ANY, podemos consultar por cualquier artículo que tiene un tag "rails":

Article.where("'rails' = ANY(tags)")

Alternatívamente, el método ALL busca por arreglos donde todos los valores en el arreglo sean iguales a los valores especificados.

Article.where("'rails' = ALL(tags)")

Como con el tipo de columna hstore, si usted esta haciendo consultas sobre un tipo de columna array, la columna puede ser indexada con GiST o GIN.

add_index :articles, :tags, using: 'gin'

###9.13.3 Tipos de direcciones de red

PostgreSQL viene con tipos de columnas exclusivos para direcciones IPv4, IPv6, y MAC. Las direcciones de host IPv4 o IPv6 son representadas con el tipo de data de Active Record inet y cidr, donde el formador acepta valores con bits distintos de cero a la derecha del netmask. Cuando Active Record recupera tipos de data inet/cidr desde la base de datos, convierte los valores a objetos IPADDr. Las direcciones MAC son representadas con el tipo de data macaddr, el cual es representado como un string en Ruby.

Para setar una columna como una dirección de red en una migración de Active Record, setee el tipo de data de la columna a inet, cidr o macaddr:

class CreateNetworkAddresses < ActiveRecord::Migration
  def change
    create_table :network_addresses do |t|
      t.inet :inet_address
      t.cidr :cidr_address
      t.macaddr :mac_address
    end
  end
end

Al setear un tipo inet o cidr a una dirección de red inválida resultara que una excepción Addr::InvalidAddressError será levantada. Si una dirección MAC inválida es seteada, un error ocurrirá a nivel de la base de datos resultando que una excepción ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation será levantada.

>> address = NetworkAddress.new
=> #<NetworkAddress id: nil, inet_address: nil, ... >

>> address.inet_address = 'abc'
IPAddr::InvalidAddressError: invalid address

>> address.inet_address = "127.0.0.1"
=> "127.0.0.1"

>> address.inet_address
=> #<IPAddr: IPv4:127.0.0.1/255.255.255.255>

>> address.save && address.reload
=> #<NetworkAddress id: 1,
      inet_address: #<IPAddr: IPv4:127.0.0.1/255.255.255.255>, ...>

###9.13.4 Tipo UUID

El tipo de columna uuid representa un identificador universalmente único (UUID), un valor de 128 bits que es generado por un algoritmo que hace que sea altamente improbable que el mismo valor sea generado dos veces.

Para setear una columna como un UUID en una migración de Active Record, setee el tipo dela columna a uuid.

add_column :table_name, :unique_identifier, :uuid

Cuando leemos o escribimos un atributo UUID, usted siempre lidiará con un string Ruby:

record.unique_identifier = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'

Si un UUID inválido es seteado un error ocurrirá a nivel de base de datos, resultando que una excepción ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation.

###9.13.5 Tipos rangos

Si usted alguna vez ha necesitado almacenar un rango de valores, Active Record ahora soporta tipos de rango PostgreSQL. Estos rangos pueden ser creados con límites inclusivos y exclusivos. Los tipos de rango siguientes son nativamente soportados:

  • daterange
  • int4range
  • int8range
  • numrange
  • tsrange
  • tstzrange

Para ilustrar, considere la aplicación de agendamiento que almacene un rango de días que represente la disponibilidad de una sala.

class CreateRooms < ActiveRecord::Migration
  def change
    create_table :rooms do |t|
      t.daterange :availability
    end
  end
end

room = Room.create(availability: Date.today..Float::INFINITY)
room.reload
room.availability # Tue, 22 Oct 2013...Infinity
room.availability.class # Range

Note que la clase Range no soporta límites inferiores exclusivos. Para más información acerca de los tipos de rango PostgreSQL, consulte la documentación oficial (http://www.postgresql.org/docs/9.3/static/rangetypes.html).

###9.13.6 Tipo JSON

Introducido en PostgreSQL 9.2, el tipo de columna json agrega la habilidad de que PostgreSQL almacene data estructurada JSON directamente en la base de datos. Cuando un objeto Active Record tiene un atributo con el tipo json, la codificación/decodificación del JSON mismo es manejada detrás de la escena por ActiveSupport::JSON. Esto le permite setear el atributo a un hash o a un string ya codificado en JSON. Si usted intenta setear al atributo JSON a un string que no pueda ser decodificado, se levantará un JSON::ParserError.

Para setear una columna como JSON en una migración Active Record, setee el tipo de data de la columna a json::

add_column :users, :preferences, :json

Para mostrar, jugemos con el atributo preferences del ejemplo previo en la consola. Para comenzar, crearé un usuario cuya preferencia de color sea azul.

>> user = User.create(preferences: { color: "blue" })
      (0.2ms) BEGIN
      SQL (1.1ms) INSERT INTO "users" ("preferences") VALUES ($1) RETURNING
      "id" [["preferences", {:color=>"blue"}]]
      (0.4ms) COMMIT
=> #<User id: 1, preferences: {:color=>"blue"}>

A continuación, verificamos cuando recuperamos el usuario desde la base de datos que el atributo preferences no retorna un string JSON sino una representación hash a cambio.

>> user.reload
    User Load (10.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> #<User id: 1, preferences: {"color"=>"blue"}>

>> user.preferences.class
=> Hash

Es importante notar que como con el tipo de data array, Active Record no da seguimiento a los cambios en el lugar. Esto significa que actualizar un hash existente no registra los cambios en la base de datos. Para asegurar que los cambios sean persistentes, debe llamar a <attribute>_will_change! o reemplazar completamente la instancia del objeto con el nuevo valor a cambio.

⚠️ **GitHub.com Fallback** ⚠️