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.
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
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)
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') }
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
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.
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) ybefore_update
(para registros existentes) -
around_create
(para registros nuevos) yaround_update
(para registros existentes) -
after_create
(para registros nuevos) yafter_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 sobreyield
-
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
ydelete_all
deActiveRecord::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ónclear
odelete
.
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.