05: ACTIVE RECORD: Trabajando con Active Record - LPC-Ltda/Ruby-on-Rails GitHub Wiki
Un objeto que envuelve una fila de una tabla o vista de una base de datos, encapsula el acceso a la base de datos, y agrega lógica de dominio sobre esa data -Martin Fowler, Patterns of Enterprise Architecture
El pattern de Active Record, identificado por Martin Fowler en su trabajo seminal, Patterns of Enterprise Architecture, mapea una clase dominio a una tabla de base de datos y una instancia de esa clase a cada fila de esa base de datos. Es una aproximación simple que, aunque no perfectamente aplicable a todos los casos, provee un marco poderoso para el acceso a la bases de datos y a la persistencia de objeto en su aplicación.
El framework Active Record de Rails es una implemantación del pattern e incluyen mecanismo para representar modelos y sus relaciones, operaciones CRUD (create, read, update, y delete), búsquedas complejas, validaciones, callbacks, y muchas otras características.
Al igual que el resto de Rails, Active Record descansa en gran medida en convención sobre configuración: Es fácil usarlo cuando usted comienza un proyecto con un nuevo schema de base de datos siguiendo estas convenciones, Sin embargo Active Record también provee seteo de configuración que le permiten adaptarlo para trabajar bien con esquemas de bases de datos heredados que no necesariamente estén conformes a las convenciones Rails.
De acuerdo a Martin Fowler, en el dicurso de apertura en la conferencia Rails en 2006, Ruby on Rails ha tomado exitosamente el patrón Active Record mucho más allá que donde cualquiera podría pensar que podía ir. Esto le muestra qué puede lograr cuando usted tiene un foco único sobre un conjunto de ideales, el cual en el caso de Rails es la simplicidad.
5.1 Lo básico
En aras de la exhaustividad, revisemos brevemente lo básico de cómo trabaja Active Record. Con el fin de crear un nueva clase de un modelo, lo primero que usted hace es declararlo como una subclase de ApplicationRecord
, usando la sintaxis de extensión de clases de Ruby.
class Client < ApplicationRecord
end
ApplicationRecord
es el modelo padre por defecto para todos los modelos a partir de Rails 5. Esta es una clase abstracta heredada deActiveRecord::Base
. Si usted viene de versiones anteriores, no se preocupe, sus modelos que heredan desdeActiveRecord::Base
funcionarán aun.
Por convención, una clase ActiveRecord llamada Client
será mapeada a la tabla clients
. Rails comprende la plurarización, tal como es cubierta en la sección "Pluralizacion" en el Capítulo 11.
También por convención, Active Record esperará una columna id
para usar como llave primaria. Este será un entero y el incremento de la llave debe ser manejado automáticamente por el servidor de base de datos cuando esté creando un nuevo registro. Note cómo la clase misma no hace mención del nombre de la tabla, columnas o sus tipos de datos.
Cada instancia de una clase Active Record provee acceso a la data de una fila de la tabla de la base de datos que la respalda, en una forma orientada al objeto. Las columnas de esa fila son representadas como atributos de el objeto, usando conversiones de tipo directo (Strings Ruby para varchar, fechas Ruby para fechas, etc.) y sin validación de data por defecto. Los atributos son inferidos desde la definición de columnas perteneciente a la tabla con la que esta relacionada. La agregación, remoción y cambio de los atributos y sus tipos es realizado al cambiar las columnas de la tabla en la base de datos.
Cuando usted está corriendo un servidor Rails en modo desarrollo, los cambios en el esquema de base de datos son reflejados en los objetos Active Record inmediatamente vía el web browser. Sin embargo, si usted hace cambios al esquema mientras usted tiene su consola Rails corriendo, los cambios no se reflejarán automáticamente, aunque es posible tomar los cambios manualmente tipeando reload!
en la consola.
Active Record es un gran ejemplo de la "Ruta Dorada" de Rails. Si se mantiene dentro de sus limitaciones, usted puede avanzar más lejos y rápido. Si se desvía del camino, usted se puede estancar en el lodo. Este camino dorado involucra muchas convenciones, como el nombrado de sus tablas en la forma plurar ("users"). Es común para desarrolladores que son nuevos en Rails y rivales de los evangelistas de los frameworks web quejarse acerca de que las tablas se puedan llamar en una forma particular, de cómo no hay restricciones a nivel de base de datos, que las llaves foráneas se manejan todas mal, que los sistemas de empresas deben tener llaves primarias compuestas, y más. Saque el reclamo ahora mismo de su sistema, porque todos esos comportamientos por defecto son simplemente "por defecto" y en la mayoría de los casos pueden ser sobre-escritos con una simple línea de código o un plugin.
5.2 Métodos estilo macro
La mayoría de las clases importantes que usted escribe mientras codifica una aplicación Rails son configuradas usando lo que yo llamo invocación de métodos estilo macro (también llamado en algunos círculos como lenguaje de dominio específico o DSL). Basicamente, la idea es tener un bloque de código altamente legible en la cima de su clase que deje inmediatamente claro como está configurada.
Las invocaciones estilo macro son usualmente ubicadas en la cima del archivos, y por buenas razones. Estos métodos declarativos le cuentan a Rails como manejar instancias, realizar validación de datos y callbacks, y relacionarse con otros modelos. Muchas de ellas hacen algo de metaprogramación, lo que significa que agregan comportamiento a su clase en tiempo de ejecución en la forma de variables de instancias y métodos.
5.2.1 Declaración de relaciones
Por ejemplo, mire la clase Client
con algunas relaciones declaradas. Hablaremos de las asociaciones en extenso en el capítulo 7, "Asociaciones Active Record", pero lo único que quiero ilustrar es a que me refiero con "estilo macro".
class Client < ActiveRecord::Base
has_many :billing_codes
has_many :billable_weeks
has_many :timessheets, through: :billable_weeks
end
Como resultado de estas tres declaraciones has_many
, la clase Client
gana al menos tres nuevos atributos -objetos proxy que le permiten manipular las colecciones asociadas interactivamente.
Aún recuerdo la primera vez que me senté con amigo un mio experimentado programador Java para enseñarle algo de Ruby on Rails. Después de minutos de profunda confusión, una ampolleta de luz casi visible apareció sobre su cabeza proclamo, "Oh, ellos son métodos!"
De hecho, son llamadas de métodos antiguos habituales, en el contexto del objeto de clase (en lugar de una de sus instancias). Dejamos los paréntesis fuera para enfatizar la intención declarativa. Es un tema de estilo, solo no me gustan con el paréntesis puesto en su lugar, como en el siguiente pedazo de código.
class Client < ActiveRecord::Base
has_many(:billing_codes)
has_many(:billable_weeks)
has_many(:timessheets, through: :billable_weeks)
end
Cuando el intérprete Ruby carga client.rb
, ejecuta estos métodos has_many
, que son definidos como métodos de la clase Base
de Active Recors. Ellos son ejecutados en el contexto de la clase Client
, agregando atributos que están subsecuentemente disponibles en el contexto en las instancias de Client
. Este es un modelo de programación que es potencialmente extraño para principiantes pero rápidamente se vuelve la segunda naturaleza del programador Rails.
5.2.2 Convención sobre configuración
Convención sobre configuración es uno de los principios guía de Ruby on Rails. Si seguimos las convenciones de Rails, muy poca configuración explícita es necesaria, lo cual contrasta con las resmas de configuración que es requerida para hacer incluso aplicaciones simples en otras tecnologías.
Esto no es que una nueva aplicación Rails con el contenido mínimo (bootstraped) venga con una configuración por defecto en su lugar ya, reflejando las convenciones que serán usadas. Es que las convenciones están respaldadas dentro del framework, codificadas en firme dentro de su comportamiento, y usted necesita sobre escribir el comportamiento por defecto con configuraciones explícitas sólo cuando aplique.
Vale la pena mencionar que la mayoría de las configuraciones ocurren con mucha proximidad a lo que usted ha configurado. Usted verá las declaraciones de asociaciones, validaciones, y callbacks en la cima de la mayoría de los modelos Active Record.
Sospecho que el primer ejemplo de configuración explícita (sobre la convención) con la que muchos de nosotros debemos lidear en Active Record es el mapeo entre el nombre de la clase y la tabla de la base de datos, ya que por defecto Rails asume que el nombre de la base de datos es simplemente la forma en plural del nombre de la clase.
5.2.3 Seteando nombres manualmente
Los métodos de seteo table_name
y primary_key
le permiten usar los nombres de tabla y llave primaria que usted guste, pero usted deberá especificarlos explícitamente en su clase modelo.
class Client < ApplicationRecord
self.table_name = "CLIENT"
self.primary_key = "CID"
end
Es sólo un par de líneas extras por modelo, pero no lo haga si no es absolutamente necesario. Un ejemplo de necesidad es en las grandes organizaciones donde usted no tiene la libertad de dictar las directrices respecto a los nombres de sus schemas de bases de datos. En muchos de esos lugares un grupo DBA separado controla todos los schemas de bases de datos. Pero si usted tiene la flexibilidad de elegir los estándar de su schema, usted debe realmente sólo seguir las convenciones de Rails. Ellas pueden no ser lo que usted solía hacer, pero seguirlas le ahorrará tiempo y dolores de cabeza.
5.2.4 Esquemas con nombres heredados
Si usted está trabajando con esquemas heredados, estará tentado de setear table_name
automáticamente por todos lados, lo necesite o no. Antes de acostumbrarse a hacer esto, aprenda las opciones adicionales disponibles que pueden ser mas DRY y harán su vida más fácil.
Asumamos que usted necesita apagar la pluralización de todas las tablas; usted puede setear el siguiente atributo en un inicializador, quizá config/initializers/legacy_settings.rb
.
Rails.application.config.active_record.pluralize_table_names = false
hay varios otros atributos útiles de ActiveRecord::Base
, provistos para configurar Rails para trabajar con esquemas con nombres heredados. Los cubriremos aquí en pos de la completitud, pero el 90% o más de los desarrolladores Rails nunca tendrá que preocuparse por estas cosas.
primary_key_prefix_type
Accesor para el tipo prefijo que se antepondrá a cada nombre de columna de llave primaria. Si :table_name
es especificado, Active Record buscará tableid
en lugar de id
como la columna primaria. Si :table_name_with_underscore
es especificado, Active Record buscará table_id
en lugar de id
.
table_name_prefix
Algunos departamentos prefijan los nombres de tablas con el nombre de la base de datos. Setee este atributo para eliminar la necesidad de incluir prefijos en todos los nombres de sus clases modelo.
**table_name_suffix
Similar al prefijo pero agrega un final común a todas los nombres de tablas.
5.3 Definiendo atributos
La lista de atributos asociados con una clase modelo de Active Record no está declarado explícitamente, a menos que usted tenga una razón para hacerlo. En tiempo de ejecución, la clase de modelo Active Record lee la información de los atributos directamente desde la definición de la base de datos. El agregar, remover y cambiar atributos y sus tipos es realizado por la manipulación de bases de datos misma vía migraciones de Active Record.
La implicancia práctica del pattern de Active Record es que usted tiene que definir su estructura de tabla de base de datos y asegurarse de existen en la base de datos antes de trabajar en sus modelos persistentes. Algunas personar pueden tener complicaciones con esta filosofía de diseño, especialmente si ellos tienen un background en diseño top-down.
El camino de Rails es indudablemente tener clases de modelos que se relacionan cercanamente su schema de base de datos. Por otro lado, recuerde que usted puede tener modelos que son clases Ruby simples y no heredan de ActiveRecord::Base
. Entre otras cosas, es común usar clases de modelos Ruby (No Active Record) para encapsular data y lógica para el nivel de vista.
5.3.1 Valores de atributos por defecto
Las migraciones le permiten definir valores de atributos por defecto al pasar una opción :default
al método column
, pero la mayor parte del tiempo usted deseará setear los valores de los atributos por defecto en el nivel de modelo, no a nivel de la base de datos. Yo siento que los valores por defecto son parte de su lógica de dominio y deben ser puestos junto al resto de la lógica de dominio de su aplicación en el nivel del modelo, En lugar de extenderse por el código base.
Un ejemplo común es el caso cuando su modelo debe retornar el string "n/a" en lugar de un nil
o string vacío para un atributo que no ha sido llenado aún. Es muy simple implementar este comportamiento declarativamente usando el nuevo atributo API del nuevo Rails 5.
class TimesheetEntry < ApplicationRecord
attribute :category, :string, default: 'n/a'
end
Pero, ¿qué sucede si está atascado en una versión anterior de Rails o si desea que el valor predeterminado dependa del valor de otros atributos en el tiempo de ejecución? El caso presenta una buena manera de aprender cómo existen atributos en objetos modelo en tiempo de ejecución.
Para empezar, ya que estamos ingresando a más de sólo una línea de código declarativo, vamos a crear una rápida especificación que describe el comportamiento que queremos. Esto a menudo se conoce como test-driven development (o TDD, para abreviar).
describe TimesheetEntry do
it "has a category of 'n/a' if not available" do
entry = TimesheetEntry.new
expect(entry.category).to eq('n/a')
end
end
Si corremos este spec fallará.
Volviendo a nuestro modelo, nortamos que los accessors de atributos son manejados usualmente "magicamente" por el interior de Active Record. En este caso, implementaremos el comportamiento de valor por defecto sobrescribiendo la magia empaquetada con un método getter explicito. Todo lo que necesitamos hacer es definir un método con el mismo nombre que el atributo y usar el operador ||
de Ruby, el cual hará "corto circuito" si @category
no es nil. Si es nil, entonces retornará el valor de la derecha.
class TimesheetEntry < ApplicationRecord
def category
@category || 'n/a'
end
end
Ahora corremos el spec y pasa. Genial. Lo hicimos? No mucho, debemos probar un caso donde un valor real de categoría sea retornado. Insertaré un ejemplo con una categoría que no sea nil:
describe TimesheetEntry do
it "returns category when available" do
entry = TimesheetEntry.new(category: "TR5W")
expect(entry.category).to eq('TR5W')
end
it "has a category of 'n/a' if not available" do
entry = TimesheetEntry.new
expect(entry.category).to eq('n/a')
end
end
Uh-oh. El primer spec falla. Parece que nuestro string por defeto "n/a" esta siendo retornado sin importar que. Lo cual significa que la variable instancia @category
no debe ser seteado donde pensábamos debía serlo. Podríamos saber siquiera si esta siendo seteado o no? Este es un detalle de implementación de Active Record o no?
El hecho de que Rails no use variables de instancia como @category
para almacenar los atributos del modelo es de hecho un detalle de implementación. Pero las instancias de modelos tienen un par de métodos -write_attribute
y read_attribute
- convenietemente provistos por Active Record con el propósito de sobre escribir los accesors por defecto, lo cual es exactamente lo que hemos tratado de hacer. Reparemos nuestra clase TimesheetEntry
.
class TimesheetEntry < ApplicationRecord
def category
read_attribute(:category) || 'n/a'
end
end
Ahora el epec pasa, y aprendimos cómo usar read_attribute
. ¿Qué tal un ejemplo simple del uso de su método hermano, write_attribute
?
class SillyFortuneCookie < ApplicationRecord
def message=(txt)
write_attribute(:message, txt + ' in bed')
end
end
Alternativamente, ambos ejemplos pueden ser escritos en las formas cortas de leer y escribir atributos, usando paréntesis cuadrados.
class TimesheetEntry < ApplicationRecord
def category
self[:category] || 'n/a'
end
end
class SillyFortuneCookie < ApplicationRecord
def message=(txt)
self[:message] = txt + ' in bed')
end
end
5.4 CRUD: Create, Read, Update, y Delete
Las cuatro operaciones estándar de un sistema de bases de datos que se combinan para formar el acrónimo popular CRUD. Esto suena a algo negativo, porque es un sinónimo de basura o acumulación indeseada, la palabra crud en ingles tiene una connotación algo negativa. Sin embargo en los círculos de Rails, el uso de la palabra CRUD es benigna. De hecho, en capítulos anteriores, diseñar su aplicación para que funcione primariamente como operaciones CRUD RESTful es considerada una buena práctica!
5.4.1 Creando nuevas instancias de modelo Active Record
La forma más directa de crear una nueva instancia de un modelo Active Record es usando un constructor regular de Ruby: el método de clase new
. Los nuevos objetos pueden ser instanciados ya sea vacíos (al omitir parámetros) o preseteados con atributos pero no grabados aún. Sólo pase un hash cuyos campos llaves coincidan con los nombres de las columnas de la tabla. En ambas instancias, las llaves de atributos válidos están determinados por los nombres de columnas de la tabla asociada -Por lo tanto usted no puede tener atributos que no sean parte de las columnas de la tabla.
Usted puede descubrir si un objeto Active Record está grabado mirando el valor de su id o, usando los métodos new_record?
y persisted?
:
c = Client.new
=> #<Client id: nil, name: nil, code:nil>
>> c.new_record?
=> true
>> c.persisted?
=> false
Los constructores de Active Record toman un bloque opcional, el cual puede ser usado para hacer la inicialización adicional. El bloque es ejecutado después de que cualquier atributo pasado dentro sea seteado sobre la instancia:
>> c = Client.new do |client|
>> client.name = "Nile River Co."
>> client.code = "NRC"
>> end
=> #<Client id:1, name: "Nile River Co.", code: "NRC">
Active Record tiene un método de clase create
que crea una nueva instancia, la hace persistente en la base de datos, y la retorna en una sola operación:
>> c = Client.create(name: "Nile River, Co.", code: "NRC")
=> #<Client id: 1, name: "Nile River, Co.", code: "NRC" ...>
El método create
toma un bloque opcional igual que el método new
.
5.4.2 Leyendo objetos Active Record
Encontrar un objeto existente por su llave primaria es muy simple y es probablemente una de las primeras cosas que todos aprendemos acerca de Rails cuando comenzamos con el framework. Sólo invoque find
con la clave de la instancia específica que usted desea recuperar. Recuerde que si no se encuentra una instancia una excepción RecordNotFound
es levantada.
>> first_project = Project.find(1)
=> #<Project id: 1 ...>
>> boom_clients = Client.find(99)
ActiveRecord::RecordNotFound: Couldn't find Client with ID-99
>> all_clients = Client.all
=> #<ActiveRecord::Relation [#<Client id: 1, name: "Paper Jam Printers", code: "PJP" ...>,
#<Client id: 2, name: "Goodness Steaks", code: "GOOD_STEAKS" ...>]>
>> first_client = Client.first
=> #<Client id: 1, name: "Paper Jam Printers", code: "PJP", ...>
A propósito, es muy común para métodos en Ruby retornar diferentes tipos dependiendo de los parámetros usados, como ilustra el ejemplo. Dependiendo de cómo es invocado find
, usted obtendrá un objeto Active Record único o un arreglo de ellos.
Por conveniencia, first
, last
y all
existen para aliviar el uso del método find
.
>> Product.last
=> #<Product id: 1, name: "leaf", sku: nil, created_at: "2010-01-12 03:34:41", updated_at: "2010-01-12 03:34:41">
Dado que el pattern subyacente es también común, el método first_or_initialize
envuelve first
, y por defecto inicializa una nueva instancia con los parámetros provistos si el conjunto de resultados está vacío.
>> Event.delete_all
SQL (9.2ms) DELETE FROM "events"
=> 5
>> Event.all
Event Load (0.1ms) SELECT "events".* FROM "events"
=> #<ActiveRecord::Relation []>
>> e = Event.first_or_initialize(start_at: Date.today)
Event Load (0.1ms) SELECT "events".* FROM "events" ORDER BY "events"."id" ASC KLIMIT ? ["LIMIT", 1](/LPC-Ltda/Ruby-on-Rails/wiki/"LIMIT",-1)
=> #<Event id: nil, starts_at: "2016-11-28", ...>
>> e.save
(0.1ms) begin transaction
SQL (0.2ms) INSERT INTO "events" ...
>> Event.first_or_initialize(starts_at: 1.year.ago)
Event Load (0.1ms) SELECT "events".* FROM "events" ORDER BY "events"."id" ASC LIMIT ? ["LINIT", 1](/LPC-Ltda/Ruby-on-Rails/wiki/"LINIT",-1)
=> #<Event id: 6, starts_at: "2016-11-28", ...>
Finalmente, el método find
también comprende arreglos de ids y levanta una excepción RecordNotFound
si no puede encontrar todos los ids específicados:
>> Product.find([1, 2])
ActiveRecord::RecordNotFound: Couldn't find all Products with IDs (1, 2)
(found 1 results, but was looking for 2)
Un primo de find
menos conocido se llama take
(desde v4.0.2). Devuelve un registro (o N registros si se proporciona un parámetro) sin ningún orden implícito, en lugar de depender de cualquier orden proporcionado por la implementación de la base de datos. Si se suministra un orden (aunque no tenga sentido hacerlo) será respetado.
# returns an objects fetched by SELECT * FROM people LIMIT 1
Person.take
# returns 5 objects fecthed by SELECT * FROM people LIMIT 5
Person.take(5)
5.4.3 Leyendo y escribiendo atributos
Después de que usted ha recuperado una instancia del modelo desde la base de datos, usted puede acceder a sus columnas de muchas formas. La más simple (y fácil de leer) es simplemente con la notación punto:
>> first_client.name
=> "Paper Jam Printers"
>> first_client.code
=> "PJP"
El método privado read_attribute
de Active Record. cubierto brevemente en una sección anterior, es útil conocerlo cuando deseamos sobre-escribir un accesor de atributo por defecto. Para ilustrar, mientras estamos aún en la consola Rails, continuo y reabro la clase Client
y sobre-escribo el accesor name
para retornar el valor desde la base de datos, sólo invertido.
>>class Client < ApplicationRecord
>> def name
>> read_attribute(:name).reverse
>> end
>>end
=> nil
>> first_client.name
=> "sretnirP maJ repaP"
Por suerte, no es demasiado doloroso para mí demostrar por qué necesita read_attribute
en ese escenario. La recursión es una perra si es inesperada:
>> class Client < ApplicationRecord
>> def name
>> self.name.reverse
>> end
>> end
=> nil
>> first_client.name
SystemStackError: stack level too deep
from (irb) :21:in 'name'
from (irb) :21:in 'name'
from (irb) :24
Como se puede esperar por la existencia de un método read_attribute
(como cubrimos en el capítulo anterior), hay también un método write_attribute
que le permite cambiar valores de atributos. Tal como lo hicimos con los métodos getter de atributos, usted puede sobre-escribir el método setter y proveer su propio comportamiento:
class Project < ApplicationRecord
# The description for a project cannot be changed to a blank string
def description=(new_value)
write_attribute(:description, new_value) unless new_value?
end
end
El ejemplo precedente ilustra una forma de hacer una validación básica, ya que chequea que el valor no sea blanco antes de permitir la asignación. Sin embargo, como verá en el capítulo 8, "Validaciones", hay mejores formas de hacerlo.
5.4.3.1 Notación hash
Otra forma de acceder a los atributos es mediante el operador [nombre_atributo]
, que le permite acceder al atributo como si fuera un hash normal.
>> first_client['name']
=> "Paper Jam Printers"
>> first_client[:name]
=> "Paper Jam Printers"
String versus Symbol. Muchos métodos Rails aceptan parámetros símbolos y strings indiferentemente, y esto puede ser muy confuso. Cual es más correcto? La regla general es: use símbolos cuando se trata del nombre de algo y strings cuando sea un valor. Usted usará símbolos cuando se trata de llaves de opciones o hashes.
5.4.3.2 El método attributes
Hay también un método attributes
que retorna un hash con cada atributo y sus valores correspondientes como son retornados por read_attribute
. Si usted usa su propios métodos de lectura y escritura personalizados, es importante recordar que attributes
no usa lectores de atributos personalizados cuando accesa sus valores, pero attributes=
(el cual le permite hacer asignaciones masivas) invoca escritores de atributos personalizados.
>> first_client.attributes
=> {"name"=>"Paper Jam Printers", "code"=>"PJP", "id"=>1}
Siendo capaz de agarrar un hash de todos los atributos de una sola vez cuando usted desea iterar sobre todos ellos o pasarlos en masa a otra función. Note que el hash retornado desde attributes
no es una referencia a una estructura interna de un objeto Active Record. Es una copia, lo cual significa que cambiar sus valores no tendrá efecto sobre el objeto desde el cual proviene.
>> atts = first_client.attributes
=> {"name"=>"Paper Jam Printers", "code"=>"PJP", "id"=>1}
>> atts["name"] = "Def Jam Printers"
=> "Def Jam Printers"
>> first_client.attributes
=> {"name"=>"Paper Jam Printers", "code"=>"PJP", "id"=>1}
Para hacer cambios a los atributos de un objeto Active Record en masa, es posible pasar un hash al escritor attributes
.
5.4.4 Accesando y manipulando atributos antes de sean convertidos en un tipo de datos (Typecast)
Los adaptadores de conexión de Active Record, clases que implementan el comportamiento específico de la base de datos, obtienen resultados como string y Rails se ocupa de convertirlos a otros tipos de datos si es necesario, basado en el tipo de columna de la base de datos. Por ejemplo, los tipos enteros son casteados a instancias de la clase de Fixnum
de Ruby , y así...
Incluso si usted está trabajando con una nueva instancia de un objeto Active Record y ha pasado en el constructor valores como string, ellos serán casteados a sus tipos propios cuando usted trate de acceder a estos valores como atributos.
Algunas veces usted deseará leer (o manipular) el dato de atributo puro sin que el cast de tipo ocurra primero, y esto puede ser hecho usando el accesor attribute_before_type_cast
que es automáticamente creado en su modelo.
Por ejemplo, considere la necesidad de tratar con strings de "tipo de cambio! tipados por su usuario final. A menos que usted esté encapsulando valores de tipo de cambio en un clase tipo de cambio (muy recomendado, por lo demás ver sección "Money Gem"), usted necesita tratar con ese maldito signo dolar y las comas. Asumamos que nuestro modelo Timesheet
tiene un atributo rate
definido del tipo :decimal
, el siguiente código le sacará los caracteres raros antes del cast de tipos para una operación segura.
class Timesheet < ApplicationRecord
before_validation :fix_rate
def fix_rate
self.[:rate] = rate_before_type_cast.tr('$, ','')
end
end
5.4.5 Volviendo a cargar (Reloading)
El método reload
hace una consulta a la base de datos y resetea los atributos de un objeto Active Record. El argumento opcional options es pasado para encontrar cuando recargar, así usted puede hacer, por ejemplo, record.reload(lock: true)
para recargar el mismo registro con un lock de fila exclusivo. (Ver la sección "Lockeo de base de datos" después en este capítulo).
5.4.6 Clonación
Producir una copia de un objeto Active Record es hecho simplemente llamando a clone
, lo cual produce una copia superficial del objeto. Es importante notar que las asociaciones no pueden ser copiadas, incluso si ellas son almacenadas internamente como variables de la instancia.
5.4.7 El Cache de consulta
Por defecto, Rails intenta optimizar el desempeño encendiendo un cache de consultas simple. Este es un hash almacenado sobre el thread actual -uno para cada conexión base de datos activa. (La mayoría de los procesos Rails tendrán sólo uno).
Donde quiera que un find
ocurra (o cualquier otro tipo de operación select) y el cache de consultas esté activo, el conjunto resultado correspondiente es almacenado en un hash con el SQL que fue usado para consultar por ellos como una llave. Si la misma instrucción SQL es usada nuevamente en otra operación, el resultado en cache es usado para generar un nuevo conjunto de objetos del modelo en lugar de volver a preguntar a la base de datos.
Usted puede habilitar el cache de consultas manualmente para envolver operaciones en un bloque cache
, como en el siguiente ejemplo:
User.cache do
puts User.first
puts User.first
puts User.first
end
Chequee su development.log
y verá las siguientes entradas:
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users", "id"
ASC LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY "users", "id"
ASC LIMIT 1 LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" ORDER BY "users", "id"
ASC LIMIT 1
La base de datos fue consultada sólo una vez. Intente un experimento similar en su consola sin el block cache
y usted verá que tres eventos User Load
serán loggeados.
Las operaciones de guardar y borrar hacen que el cache sea limpiado para prevenir la propagación de instancias con estados no válidos. Si usted encuentra necesario hacerlo por cualquier razón, llame al método de clase
clear_query_cache
para limpiar el cache de consultas manualmente.
5.4.7.1 Loggeo
El archivo log muestra cuando la data está siendo leída desde el cache de consultas en lugar de la base de datos. Sólo mire las líneas que comienzan con CACHE en lugar de Modelo Load.
Place Load (0.1ms) SELECT * FROM places WHERE (places.id = 15749)
CACHE (0.0ms) SELECT * FROM places WHERE (places.id = 15749)
CACHE (0.1ms) SELECT * FROM places WHERE (places.id = 15749)
5.4.7.2 Caching de consultas por defecto en controladores
Por razones de desempeño, el cache de consultas de Active Record está encendido por defecto para el procesamiento de acciones del controlador.
5.4.7.3 Limitaciones
El caché de consultas de Active Record se mantuvo deliberadamente muy simple. Dado que literalmente almacena el modelo en caché en la instancia de SQL que se usó para sacarlos de la base de datos, no puede conectar múltiples invocaciones de búsqueda que están expresadas de manera diferente pero tienen el mismo significado y resultados semánticos.
Por ejemplo, "select foo from bar where id=1" y "select foo from bar where id = 1 limit 1" son consideradas diferentes consultas y utilizarán diferentes entradas en el cache.
5.4.8 Actualizar
La forma más simple de manipular valores de atributos es simplemente tratar su objeto Active Record como un viejo objeto Ruby plano, lo cual significa, vía asignación directa usando myprop=(some_value)
.
Hay un número de otras formas diferentes de actualizar un objeto Active Record, como se ilustra en esta sección. Primero, miremos como usar el método de clase update
de ActiveRecord::Base
:
class ProjectController < ApplicationController
def update
Project.update(params[:id], params[:project])
redirect_to projects_path
end
def mass_update
Project.update(params[:projects].keys, params[:projects].values])
redirect_to projects_path
end
La primera forma de update
toma un id numérico único y un hash de valores de atributos, mientras que una segunda forma toma una lista de ids y una lista de valores y es útil en escenarios donde el envío de un formulario desde una página web con múltiples filas modificables está siendo procesado.
El método de clase update
invoca la validación primero y no guarda un registro que falla en la validación. Sin embargo, retorna el objeto sin importar si la validación pasa o no. Esto significa que si usted desea conocer si la validación pasó o no, necesita dar seguimiento a la llamada update
con una llamada a valid?
.
class ProjectController < ApplicationController}
def update
project = Project.update(params[:id], params[:project])
if project.valid? # Uh-oh, do we want to run validate again?
redirect_to project
else
render 'edit'
end
end
end
Un problema es que ahora estamos llamando a valid?
dos veces, ya que update
también lo llama. Quizá una mejor opción es usar un método de instancia update
una vez como parte de una instrucción if
:
class ProjectController < ApplicationController
def update
project = Project.find(params[:id])
if project.update(params[:project])
redirect_to project
else
render 'edit'
end
end
end
Y por supuesto, si usted ha realizado algo de programación básica de Rails, reconocerá este patrón, ya que es usado en el código scaffolding generado. El método update
toma un hash de valores de atributos y retorna verdadero o falso, dependiendo de si grabó exitosamente o no, lo cual depende de si pasó la validación o no.
5.4.9 Actualizando por condición
Active Record tiene otro método de clase útil para actualizar múltiples registros de una vez: update_all
. Este mapea muy parecido a la forma en que usted pensaría usar una instrucción update... where
de SQL. El método update_all
toma dos parámetros la parte set de la instrucción SQL y la condición, expresada como parte de una cláusula where. El método retorna el número de registros actualizados.
Pienso que este es uno de esos métodos que es generalmente más útil en un contexto de scripting que en un método de controlador, pero usted puede sentir diferente. Aquí hay un ejemplo rápido de cómo puedo reasignar todos los proyectos en el sistema a un nuevo administrador de proyectos.
Project.update_all({manager: 'Ron Campbell'}, technology: 'Rails')
El método update_all
también acepta parámetros string, los cuales le permite aumentar la potencia de SQL!
Project.update_all("cost=cost*3", "lower(technology) LIKE '%microsoft%'")
5.4.10 Actualizando una instancia particular
La forma más básica de actualizar un objeto Active Record es manipular sus atributos directamente y luego llamar a save
. Es bueno notar que save
insertará un registro en la base de datos si es necesario o actualizará un registro existente con la misma llave primaria.
>> project = Project.find(1)
>> project.manager = 'Brett M.'
>> project.save
=> true
El método save
retornará true
si fue exitoso y false
si falló por alguna razón. Hay otro método, save!
, que usará excepciones en cambio. Cual usar depende de si usted planea tratar con los errores directamente o delegar el problema a otro método adicionado a la cadena.
Esto es más una preocupación de estilo, aunque los métodos save
y update
no-bang que retornan un valor booleano son frecuentemente usados en acciones de controlador, como cláusula para una condición if:
class StoryController < ApplicationController
def points
story = Story.find(params[:id])
if story.update_attribute(:points, params[:value])
render text: "#{story.name} updated"
else
render text: "Error updating story points"
end
end
end
5.4.11 Actualizando atributos específicos
Los métodos de instancia update_attribute
y update
toma un par llave/valor o un hash de atributos, respectivamete, para ser actualizado en su modelo y guardado en la base de datos en una operación.
El método update_attribute
actualiza un atributo único y guarda el registro, pero las actualizaciones realizadas con este método no están sujetas a chequeo de validación. En otras palabras, este método lo permite hacer persistente un modelo de Active Record en la base de datos incluso si el objeto completo es no-válido. Los callback del modelo son ejecutados pero el updated_at
aún es golpeado.
Me siento sucio cuando uso
update_attributer
.
Por otro lado. update
es sujeto de chequeo de validación y es frecuentemente usado en acciones update y se le pasa el hash params que contiene los valores actualizados.
Active Record también provee un método de instancia update_column
, el cual acepta un par llave/valor simple. Aunque similar a update_attribute
, el método update_column
no sólo se salta el chequeo de validaciones sino que además no corre callbacks y se salta golpear el timestamp updated_at
.
Rails 4 introduce el método update_columns
, el cual trabaja exáctamente igual a update_column
, pero en lugar de aceptar un par llave/valor único como un parámetro, acepta un hash de atributos.
Si usted tiene asociaciones en el modelo, Active Record automáticamente crea los métodos convenientes para la asignación masiva. En otras palabras, un modelo
Project
quehas_many :users
expondrá un atributo de escriturauser_ids
, el cual será usado para su métodoupdate
. Esta es una ventaja si usted está actualizando asociaciones con checkboxes, porque usted sólo nombra las cajas de chequeoproject[user_ids][]
y Rails manejará la magia. En algunos casos, permitir al usuario setear asociaciones de esta forma puede ser un riesgo de seguridad.
5.4.12 Guardando sin actualizar el Timestamp
Rails 5 agrega una opción touch
para save
, la cual le da la opción de hacer una operación update sin actualizarl el timestamp updated_at
del registro. Simplemente pase touch: false
y recuerde que esto sólo funciona en update, no cuando insertamos un nuevo registro.
>> user = User.first
>> user.updated_at
=> Wed, 16 Mar 2016 09:12:44 UTC +00:00
>> user.notes = "Hide this note from auditors"
>> user.save(touch: false)
UPDATE "users" SET "notes" = ? WHERE "users"."id" = ?
["notes", "Hide this note from auditors"], ["id", 12](/LPC-Ltda/Ruby-on-Rails/wiki/"notes",-"Hide-this-note-from-auditors"],-["id",-12)
=> true
Al igual que algunas características más pequeñas en Rails, me resulta difícil imaginar cómo el "no-touch" podría ser útil.
5.4.13 Actualizadores de conveniencia
Rails provee un número de métodos de actualización de conveniencia en forma de increment
, decrement
y toogle
, los cuales hacen exactamente lo que sus nombres sugieren con atributos numéricos y booleanos. Cada uno tiene sus variantes bang (como toggle!
) que adicionalmente invoca a update_attribute
después de modificar el atributo.
5.4.14 Tocando (Touching) registros
Hay ciertos casos donde actualizar un campo hora para indicar que el registro fue visto es lo único que usted requiere, y Active Record provee un método conveniente para hacerlo en la forma de touch
. Esto es especialmente útil para la auto-expiración del cache, lo cual es cubierto en el Capitulo 17, "Cache y desempeño".
Al usar este método sin argumentos actualiza el timestamp updated_at
a la hora actual sin gatillar ningún callback ni validación. Si un atributo timestamp es entregado, actualizará ese atributo a la hora actual junto con updated_at
.
>> user = User.first
>> user.touch # => sets updated_at to now.
>> user.touch(:viewed_at) # sets viewed_at y updated_at to now
(lo que viene se sacó del último libro, lo dejo por si acaso)
Si una opción :touch
es provista a una relación belong_to
, hara un touch al padre cuando al hijo sea tocado (touched).
class User < ActiveRecord::Base
belong_to :client, touch: true
end
>> user.touch # => also calls user.client.touch
readonly
5.4.15 Atributos Algunas veces usted desea designar ciertos atributos como readonly
, lo cual previene que sean actualizados después de que el objeto padre es creado. La característica es principalmente para ser usada en conjunto con atributos calculados. De hecho, Active Record usa internamente este atributo para atributos counter_cache
, ya que ellos son mantenidos con sus propias instrucciones especiales SQL de actualización.
El único momento en el que un atributo read-only puede ser seteado es cuando el objeto no ha sido guardado aún. El siguiente ejemplo ilustra el uso de attr_readonly
. Note el potencial que se obtiene cuando se trata de actualizar el atributo read-only
.
class Customer < ApplicationRecord
attr_readonly :social_security_number
end
>> customer = Customer.new(social_security_number: "130803020")
=> #<Customer id: 1, social_security_number: "1300803020", ...>
>> customer.social_security_number
=> "1300803020"
>> customer.save
>> customer.social_security_number = "0000000000" # Note, no error raised!
>> customer.social_security_number
=> "0000000000"
>> customer.save
>> customer.reload
>> customer.social_security_number
=> "1300803020" # the original readonly value is preserved
El hecho es que tratar de setear un nuevo valor para un atributo readonly
no levante un error molesta mi sensibilidad, pero entiendo como esto hace el uso de esta característica un poco menos intensiva en código.
Usted puede obtener una lista de todos los atributos read-only
vía el método de clase readonly_attributes
.
>> Customer.readonly_attributes
=> #<Set: {"social_security_number"}>
5.4.16 Borrando y Destruyendo
Finalmente, si usted desea remover un registro desde su base de datos, usted tiene dos opciones: delete o destroy. Si usted ya tiene una instancia del modelo puede destruirla:
>> bad_timesheet = Timesheet.find(1)
>> bad_timesheet.destroy
=> #<Timesheet id: 1, user_id: "1", submitted: nil,
created_at: "2006-11-21 05:40:27", updated_at: "2006-11-21 05:40:27">
El mátodo destroy
removerá el objeto de la base de datos y prevendrá que usted lo modifique nuevamente:
>> bad_timesheet.user_id = 2
RuntimeError: can't modify frozen Hash
Note que llamar a save
sobre un objeto que ha sido destruido fallará silenciosamente. Si usted necesita chequear si un objeto ha sido destruido usted puede ser el método destroyed?
El método destroy
también tiene un método bang complementario, destroy!
. Al llamar a destroy!
sobre un objeto que no puede ser destruido hará que una excepción Record::RecordNotDestroyed
será levantada.
Usted también también puede llamar a destroy
y delete
como métodos de clase, pasándole el o los id(s) a borrar. Ambas variantes aceptan un parámetro único o un arreglo de ids:
Timesheet.delete(1)
Timesheet.destroy([2, 3])
Los nombres pueden parecer inconsistentes, pero no lo es. El método delete
usa SQL directamente y no carga ninguna instancia (así que es más rápido). El metodo destroy
carga la instancia del objeto Active Record y entonces llama a destroy
como un método de instancia. La diferencia semántica son sutiles pero entran en juego cuando usted ha asignado callbacks before_destroy
o tiene asociaciones de dependencia -objetos hijos que deben ser borrados automáticamente junto a su objeto padre.
5.5 Bloqueo (Locking) de base de datos
Bloqueo (Locking) es un término para las técnicas que previenen que usuarios concurrentes de una aplicación sobre-escriban el trabajo del otro. Active Record normalmente no usa ningún tipo de lockeo de base de datos cuando carga filas de data de un modelo desde una base de datos. Si una aplicación Rails dada tendrá sólo un usuario actualizando data al mismo tiempo, entonces usted no debe preocuparse por esto.
Sin embargo, cuando más de un usuario puede estar accesando y actualizando al mismo tiempo data simultáneamente, entonces es de vital importancia para usted como desarrollador pensar en la concurrencia. Pregúntese a si mismo que tipo de colisiones o condiciones de carrera pueden ocurrir si dos usuarios tratan de actualizar un modelo dado al mismo tiempo?
Hay un número de aproximaciones para tratar la concurrencia en las aplicaciones de bases de datos, dos de las cuales están soportadas nativamente por Active Record: bloqueo optimista y pesimista. Existen otras aproximaciones, como bloquear las tablas de bases de datos completas. Cada aproximación tiene fortalezas y debilidades, por lo que es probable que una aplicación use una combinación de aproximaciones para máximizar la confiabilidad.
5.5.1 Bloqueo optimista
El bloqueo optimista describe la estrategia de detectar y resolver colisiones si ellas ocurren y es comúnmente recomendado en situaciones multiusuarios donde las colisiones son poco frecuentes. Los registros de la base de datos no están realmente nunca bloqueados en el bloqueo optimista, haciendo que el nombre sea un poco inapropiado.
El bloqueo optimista es una estrategia bastante común debido a que muchas aplicaciones se han diseñado de tal manera que un usuario particular estará principalmente actualizando los datos que conceptualmente le pertenecen a él y no a otros usuarios, haciendo que sea raro que dos usuarios puedan competir por actualizar un mismo registro. La idea detrás el bloqueo optimista es que ya que las colisiones ocurrirán con poca frecuencia, trataremos con ellas sólo si ellas ocurren.
5.5.1.1 Implementación
Si usted controla el schema de su base de datos, el bloqueo optimista es muy simple de implementar. Sólo agregue una columna del tipo integer llamada lock_version
a una tabla dada, con un valor por defecto de cero.
class AddLockVersionToTimesheet < ActiveRecord::Migration
def change
add_column :timesheet, :lock_version, :integer, default: 0
end
end
Con sólo agregar esta columna lock_version
cambia el comportamiento de Active Record. Ahora si el mismo registro es cargado como dos instancias diferentes del modelo y grabadas, la primera instancia ganará la actualización, y la segunda podría causar que se levante una excepción ActiveRecord::StaleObjectError
.
Podemos ilustrar el comportamiento del lockeo optimista con un spec simple:
describe Timesheet do
it "locks optimistically" do
t1 = Timesheet.create
t2 = Timesheet.find(t1.id)
t1.rate = 250
t1.rate = 175
expect(t1.save).to be_true
expect(t2.save).to raise_error(ActiveRecord::StaleObjectError)
end
end
El spec pasa, porque al llamar save
sobre la segunda instancia levanta la excepción ActiveRecord::StaleObjectError
. Note que el método save
(sin un bang) retorna false y no levanta una excepción si el save
falla debido a una validación, pero otros problemas, como el bloqueo en este caso, pueden en efecto causar el levantamiento de una excepción.
Para usar una columna de la base de datos llamada distinto a lock_version
, cambie el seteo usando locking_column
. Para hacer el cambio globalmente, agregue la siguiente línea a un inicializador:
Rails.application.config.active_record.locking_column = :alternate_lock_version
Como otros seteos Active Record, usted puede también cambiarlo con base en el modelo usado con una declaración en su clase de modelo:
class Timesheet < ActiveRecord::Base
self.locking_column = :alternate_lock_version
end
5.5.1.2 Manejando StaleObjectError
Ahora por supuesto, después de agregar el lockeo optimista, usted no deseará sólo dejarlo en eso, o que el usuario final que pierde en la colisión simplemente vea una pantalla de error. Usted debe tratar de manejar el StaleObjectError
con tanta gracia como le sea posible.
Dependiendo de la criticidad de la data que está siendo actualizada, usted deseará invertir tiempo en diseñar una solución amigable para el usuario que de alguna forma preserve los cambios que el perdedor intentó hacer. Como mínimo, si la data para la actualización es fácil de volver a construir, hágale saber al usuario porque su actualización falló con un código del controlador que luzca como lo siguiente:
def update
timesheet = Timesheet.find(params[:id])
timesheet.update(params[:timesheet])
# redirect somewhere
rescue ActiveRecord::StaleObjectError
flash[:error] = "Timesheet was modified while you where editing it."
redirect_to [:edit, timesheet]
end
Hay algunas ventajas en el bloqueo optimista. No requiere ninguna característica especial en la base de datos, y es muy fácil de implementar. Como usted vio en el ejemplo, muy poco código es necesario para manejar el StaleObjectError
.
La principal desventaja del bloqueo optimista es que las operaciones update son un poco lentas porque el lock version debe ser chequeado y el potencial para una mala experiencia de usuario, ya que los usuarios no saben de la falla hasta que potencialmente pierden data.
5.5.2 Bloqueo (Locking) pesimista
El bloqueo pesimista requiere un soporte especial de base de datos (construido en la principales bases de datos) y bloquear filas específicas de la base de datos durante la operación de actualización. Esto previene que otro usuario lea data que está siendo actualizada con el fin de prevenirlos de trabajar con data robada.
El lockeo pesimista trabaja en conjunción con transacciones como en el siguiente ejemplo:
Timesheet.transaction do
t = Timesheet.lock.first
t.approved = true
t.save!
end
Es también posible llamar a lock!
sobre una instancia de modelo existente, la cual simplemente llama a reload(lock: true) bajo la cubierta. Usted no deseará hacer esto en una instancia con cambios de atributos ya que esto podría causar que se perdieran al volver a cargar. Si usted decide que no desea bloquear más, puede pasar
falseal método
lock!`.
El lockeo pesimista toma lugar en el nivel de la base de datos. La instrucción SELECT generada por Active Record tendrá una clausula FOR UPDATE (o similar) agregada a él, causando que todas las otras conexiones serán bloqueadas de acceso a las filas retornadas por la instrucción select. El lock es liberado una vez que se hace un commit de la transacción. Teóricamente hay situaciones (el proceso Rails hace boom en medio de la transacción) donde el lock no será liberado hasta que la conexión sea terminada u ocurra el time out.
5.5.3 Consideraciones
Las aplicaciones web escalan mejor con bloqueo optimista, el cual, como hemos discutido no utiliza ningún bloqueo a nivel de base de datos. Sin embargo, usted tiene que agregar lógica de aplicación para manejar los casos de falla. El bloqueo pesimista es un poco más simple de implementar pero puede conducir a situaciones donde un proceso Rails este esperando que otro libere el lock de la base de datos, esto es, espeando sin servir ningun otro requerimiento entrante. Recuerde que los procesos Rails son típicamente de thread único.
En mi opinión, el lockeo pesimista no es muy peligroso como lo es en otras plataformas, ya que en Rails no haremos persistente transacciones de bases de datos a través de más de un requerimiento HTTP. De hecho, es imposible hacer esto en una arquitectura que no comparte nada. (Si usted está corriendo Rails con JRuby y haciendo cosas locas como almacenar instancias de objetos Active Record en el espacio de una sesión compartida, todas las apuestas están en contra).
Una situación para ser cauteloso será una donde usted tenga muchos usuarios compitiendo para acceder a un registro particular que tomo tiempo actualizar. Para mejores resultados, haga pequeña su transacción bloqueada en forma pesimista y asegúrese que se ejecuta rápidamente.
5.6 Consultar
Cuando mencionamos el método find
de Active Record anteriormente en el capítulo, no miramos la riqueza de opciones disponibles adicionalmente al consultar por la llave primaria y a los métodos first
, last
y all
. Cada método discutido aquí retorna un ActiveRecord::Relation
-un objeto encadenable que es perezosamente evaluado contra la base de datos sólo cuando los registros reales son necesitados.
Las consultas de Active Record y el comportamiento de las relaciones son implementadas usando la gema de álgebra relacional Arel, la cual es considerada parte de Rails y mantenido por el equipo del núcleo de Rails. Intentamos proveerle con un resumen completo en este capítulo, pero una cobertura completa y una explicación de todo lo posible con Arel puedo requerir un libro sólo para ello.
Tenga en cuenta que he intentado ordenar la lista de métodos por importancia relativa para la tarea de codificación diaria.
where(*conditions)
5.6.1 Es muy común necesitar filtrar el conjunto resultado de una operación find (sólo un SELECT de SQL bajo la cubierta) agregando condiciones (a la cláusula WHERE). Active Record le da una cantidad de formas de hacer esto con el método where
.
El parámetro conditions
puede ser especificado como un string o un hash. Los parámetros son inmediatamente sanitizados para evitar un ataque de inyección de SQL.
Al pasar un hash de condiciones construirá una cláusula where que contiene la unión de todos los pares llave/valor. Si todo lo que usted necesita es igualdad, versus digamos, un criterio LIKE, le aconsejo que use la notación hash, ya que es el más legible de los estilos.
Product.where(sku: params[:sku])
La notación hash es suficientemente inteligente para crear una cláusula IN si usted asocia un arreglo de valores con una llave particular.
Product.where(sku: [9400,9500,9900])
La forma string simple puede ser usada para instrucciones que no involucran data originada fuera de su aplicación. Es más útil para hacer comparaciones LIKE, así como greater-than/less-than y para el uso de funciones SQL no empaquetadas en Active Record, como aquellas que se necesitan para consultar dentro de columnas Hstore y JSON en PostgrSQL.
Si usted elige usar el estilo string, los argumentos adicionales para el metodo where
son tratados como variables de consulta a insertar dentro de la cláusula where
.
Product.where('description like ? and color = ?', "%#{terms}%", color)
Product.where('sku in (?)', selected_skus)
User.where('preferences @> ?', {newsletter: true}.to_json)
Note que dates, booleans, y arrays como selected_skus
son llevados a sus representaciones dentro de una expresión SQL correcta y automaticamente.
where.not
. La consulta de Active Record interfacea la mayoría de las partes abstractas de SQL desde el desarrollador. Sin embargo, hay una condición que siempre requiere usar una condición string pura en una cláusula where, específicamente una condición NOT con <> O !=, dependiendo de la base de datos. Partiendo con Rails 4, el método de consulta not
ha sido agregado para rectificar esto.
Para usar el nuevo método de consulta, debe ser encadenado a la cláusula where
sin argumentos:
Article.where.not(title: 'Rails 3')
# >> SELECT "articles".* FROM "articles"
WHERE ("articles"."title" != 'Rails 3')
El método de consulta not
puede también aceptar un arreglo para asegurarse que múltiples valores no estén en el campo:
Article.where.not(title: ['Rails 3', 'Rails 5'])
# >> SELECT "articles".* FROM "articles"
WHERE ("articles"."title" NOT IN ('Rails 3', 'Rails 5'))
5.6.1.1 Variables bind
Cuando usamos parámetros múltiples en la condición, puede ser difícil de comprender que se supone que los cuatro a cinco signos de interrogación representan exáctamente.
En esos casos, usted puede cambiarlos por variables bind nombradas. Eso se hace al reemplazar los signos de interrogación con símbolos y entregar un hash con valores para los símbolos coincidentes como segundo parámetro.
Product.where("name = :name AND sku = :sku AND created_at > :date",
name: "Space Toilet", sku: 80800, date: '2009-01-01')
Durante una rápida discusión en IRC acerca de esta forma final, Robby Russell me dio el siguiente retazo inteligente:
Message.where("subject LIKE :foo OR body LIKE :foo", foo: '%woah%')
En otras palabras, cuando usted está usando ´placeholders nombrados (versus caracteres de signo de interrogación), usted puede usar la misma variable bind más de una vez. Como, whoa!
Condiciones hash simples como esta son muy comunes y útiles, pero ellas pueden sólo generar condiciones basadas en la igualdad con el operador SQL AND
.
User.where(login: login, password: password).first
Si usted desea una lógica distinta a AND, usted tendrá que usar una de las otras formas disponibles.
5.6.1.2 Condiciones booleanas
Es particularmente importante tener cuidado en las especificaciones de condiciones que incluyan valores booleanos. Las bases de datos tienen diferentes formas de representar valores booleanos en las columnas. Algunas tienen tipos de datos booleanos nativos, y otras usan un carácter único, frecuentemente 1
y 0
o, T
y F
(incluso Y
y N
). Rails maneja transparentemente los temas de conversión de datos si usted pasa un objeto booleano Ruby como su parámetro:
Timesheet.where('submitted = ?', true)
5.6.1.3 Condiciones nil
El experto en Rails Xavier Noria nos recuerda tener cuidado con condiciones específicas que puedan ser nil. Al usar un signo de interrogación no le permitimos a Rails imaginar que un nil
es proporcionado como valor de una condición será probablemente traducido a un IS NULL
en la consulta SQL resultante.
Compare los dos ejemplos siguientes de find y sus correspondientes consultas SQL para comprender este resultado común. El primer ejemplo no trabaja como pretende, pero el segundo si.
>> User.where('email = ?', nil)
User Load (151.4ms) SELECT * FROM users WHERE (email = NULL)
>>User.where(:email => nil)
User Load (15.2ms) SELECT * FROM users WHERE (user.email IS NULL)
5.6.2 `order(*clauses)
El método order
toma uno o más símbolos (que representan nombre de columnas) o un fragmento de SQL, especificando el orden deseado para el conjunto resultado:
Timesheet.order('created_at desc')
La especificación de SQL asume por defecto el orden ascendente si la opción es omitida, lo cual es exactamente lo que ocurre si usted usa símbolos.
# first two timesheets ever created
Timesheet.order(:created_at).take(2)
A partir de Rails 4, order
puede también aceptar argumentos hash, eliminando la necesidad de escribir SQL para las cláusulas descendientes.
Timesheet.order(created_at: :desc)
Las especificaciones de SQL no prescriben ningún ordenamiento particular si no hay una cláusula "order by" especificada en la consulta. Esto hace tropezar a la gente, ya que existe la creencia común de que "ORDER BY id ASC" esta por defecto.
El valor de la opción :order
no es validado por Rails, lo cual significa que usted puede pasar cualquier código que sea entendido por la base de datos subyacente, no sólo tuplas columna/dirección. Un ejemplo de por que esto es útil es cuando se desea obtener un registro aleatorio:
# MySQL
Timesheet.order('RAND()')
# Postgres
Timesheet.order('RANDOM()')
# Microsoft SQL Server
Timesheet.order('NEWID()') # uses random uuids to sort
# Oracle
Timesheet.order('dbms_random.value').first
Recuerde que ordenar grandes bases de datos aleatoriamente es sabido que funciona terriblemente mal en la mayoría de las bases de datos, particularmente MySQL.
Una forma inteligente, de buen desempeño y portable de obtener un registro aleatorio es generar un desplazamiento aleatorio en Ruby.
Timesheet.limit(1).offset(rand(Timesheet.count)).first
take(number)
y `skip(number)
5.6.3 El método take
(con alias limit
) toma un valor entero estableciendo un límite sobre el número de filas a retornar desde la consulta. Su compañero, el método skip
(con alias offset
), el cual puede ser encadenado a take
, especifica el numero de filas a saltar en el conjunto resultado y es 0-indexado (Al menos lo es en MySQL. Otras bases de datos pueden ser 1-indexadas.) Juntas estas opciones son útiles para paginar resultados.
No haga la paginación de sus modelos manualmente. Use la gema Kaminari en https://github.com/amatsuda/kaminari.
Por ejemplo, una llamada para encontrar la segunda página de 10 resultados en una lista de timesheets es la siguiente:
Timesheet.take(10).skip(10)
Dependiendo de lo particular del modelo de datos de su aplicación, puede tener sentido poner un límite a la cantidad máxima de objetos Active Record alcanzados en una consulta específica. Dejar que los usuarios gatillen consultas sin límites empujando miles de objetos Active Record a Rails al mismo tiempo es una receta para el desastre.
select(*clauses)
5.6.4 Por defecto, Active Record genera consultas SELECT * FROM, pero esto puede ser cambiado si, por ejemplo, usted desea hacer un join pero no incluir las columnas joineadas o si usted desea agregar una columna calculada a su conjunto resultado, como esto:
>> b = BillableWeek.select("mon_hrs + tues_hrs as two_day_total").first
=> #<BillableWeek ...>
>> b.two_day_total
=> 16
Ahora si usted realmente desea usar objetos con atributos adicionales que usted ha agregado vía el método select
, no olvide la cláusula *
:
>> b = BillableWeek.select(:*, "mon_hrs + tues_hrs as two_day_total").first
=> #<BillableWeek id: 1 ...>
Tenga en mente que las columnas no especificadas en la consulta, ya sea por * o explícitamente, no serán poblada en los objetos resultantes! Asi, por ejemplo, continuando con el primer ejemplo, al tratar de acceder a created_at
en b
tiene un resultado inesperado:
ActiveModel::MissingAttributeError: missing attribute: created_at
La razón por la que falta el atributo es porque el conjunto de resultados devuelto por la base de datos gobierna qué atributos se crean en los objetos Active Record. Ellos son esos los estrechamente vinculados al esquema de la base de datos.
from(*tables)
5.6.5 El método from le permite modificar la parte del nombre(s) de tabla de las sentencias de SQL generadas por Active Record.
Usted puede proveer un valor personalizado si usted necesita incluir tablas extra para joins o para referenciar una vista de base de datos o subconsulta.
>> Topic.select('title').from(Topic.approved).to_sql
=> "SELECT title FROM (SELECT * FROM topics WHERE approved = 't')"
También puede usarlo para anular los alias generados por su cuenta, lo que probablemente no sea muy útil, pero podría proporcionar algunos beneficios de legibilidad en ciertas situaciones que involucran joins complejos.
>> Topic.select('a.title').from(Topic.approved, a:).to_sql
=> "SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't')"
La capacidad de controlar la clausula FROM significa que usted puede facilmente construir todo o parte de su dinámica. Aquí esta un ejemplo desde una aplicación que habilita el tagging sobre una variedad de modelos distinos:
def self.find_tagged_with(list)
select("#{table_name}.*).
from("#{table_name}, tags, taggins").
where("#{table_name}.#{primary_key} = taggins.taggable_id) and taggins.tag.id = tags.id and tags.name IN (?)",
Tag.parse(list))
end
Este código ejemplo está mezclado dentro de una clase objetivo usando módulos de Ruby. Aprenda como usar esa técnica usted mismo en la sección "Modulos para reusar Comportamiento en común" en el Capítulo 9, "Active Record avanzado"
group(*args)
5.6.6 group
especifica una cláusula SQL GROUP BY
para agregar a la consulta generada por Active Record. Generalmente usted deseará combinar :group
con la opción :select
, ya que el SQL válido requiere que todas las columnas seleccionadas en un SELECT agrupado sean funciones agregadas o columnas.
>> users = Account.select('name, SUM(cash) as money').group('name').to_a
=> [#<User name: "Joe", money: "3500">, #<User name: "Jane", money: "9245">]
Tenga en mente que estas columnas extras que usted recupera pueden algunas veces ser strings si es que Active Record no trata de cambiarles el tipo. En estos casos, usted tendrá que usar to_i
y to_f
para convertir strings a tipos numéricos.
>> users.first.money > 1_000_000
ArgumentError: comparison of string with 1000000 failed from (irb):8:in '>'
distinct
5.6.7 Si usted necesita realizar una consulta con una cláusula DISTINCT SQL
, usted puede usar el método distinct
.
>> User.select(:login).distinct
User Load (0.2ms) SELECT DISTINCT login FROM "users"
having(*clauses)
5.6.8 Si usted necesita realizar una consulta agrupada con una cláusula SQL HAVING
, use el método having
:
>> User.group("created_at").having(["created_at > ?", 2.days.ago])
=> [#<User name: "Joe", created_at: "2013-03-05 19:30:11">]
includes(*associations)
5.6.9 Active Record tiene la habilidad de eliminar "N+1" consultas al dejar que usted especifique que asociaciones "cargar ansiosamente" usando el método include
u opción en sus buscadores. Active Record cargará esta relaciones con el mínimo número de consultas posible.
Para "cargar ansiosamente" asociaciones de primer grado, provea al include
un arreglo de nombres de asociaciones. Al acceder a estos más adelante en el mismo ciclo de solicitud, se necesitarán más consultas de la base de datos.
>> users = User.where(login: "mack").includes(:billable_weeks)
=> [#<User login: "mack">]
>> users.first.billable_weeks.each { |week| puts week }
=> #<Week start_date: "2008-05-01 00:00:00">
Para asociaciones de segundo grado, entregue un hash con el arreglo como valor para la llave del hash.
>> clients = Client.includes(users: [:avatar])
=> [#<Client id: 1, name: "Hashrocket">]
Usted puede agregar más inclusiones siguiendo el mismo patrón.
>> Client.includes(users: [:avatar, { timesheet: billeable_weeks }])
=> [#<Client id: 1, name: "Hashrocket">]
Si es posible, los includes
usan LEFT OUTER JOIN
para capturar todos los datos que necesita en una consulta. Cuando eso sucede, delega a eager_load
. De otra manera. Utilizará al menos dos consultas separadas y delega a preload
.
Si sabe que desea un enfoque versus el otro, puede asegurarse de obtenerlo utilizando eager_load
o preload
directamente con la misma sintaxis.
eager_load(*associations)
5.6.10 Como ya mencione, eager_load
agarra toda la data junta en una única consulta usando joins, en lugar de usar consultas separadas como lo hace preload
.
5.6.11 `preloads(*associations)
Como ya mencioné, preloads
usa consultas separadas para precargar data asociada más que intentar traer toda la data de vuelta junta en una consulta única usando joins como lo hace eager_load
No hablamos mucho mucho acerca de esto en nuestra descripción de includes
, así que proveemos un ejemplo.
>> User.preload(:auctions).to_a
User Load (0.1ms) SELECT "users".* FROM "users"
Auction Load (0.2ms) SELECT "auctions".* FROM "auctions"
WHERE "auctions"."users_id" IN (1, 2)
Note las dos consultas SQL separadas. Logicamente, ya que las consultas están separadas, usted no se puede referir a una tabla precargada en una expresión de consulta en la forma en que usted puede usar includes
.
references(*table_names)
5.6.12 El método de consulta references
es usado para indicar que una tabla relacionada es referenciada por alguna parte de la expresión SQL en construcción. Esto es sólo necesario cuando Active Record no puede imaginar que tabla debe hacer join con la propia, como en el siguiente ejemplo.
>> User.includes(:auctions).where('auction.name = ?', 'Lumina')
User Load (0.2ms) SELECT "users".* FROM
"users" WHERE (auctions.name = 'Lumina')
ActiveRecord:StatemenInvalid: SQLite3::SQLException: no such column: auctions.name
...
Quizás se esté preguntando por qué Active Record no pudo darse cuenta de que necesitaba la tabla auctions
basado en la llamada a includes
. Tampoco estoy seguro y no pude encontrar una buena respuesta. Sé que para que el ejemplo anterior funcione, necesita usar referencias con el nombre de la tabla para unirse.
>> User.includes(:auctions)
.where('auctions_name = ?', 'Lumina')
.references(:auctions)
Una mejor y más consisa alternativa para el uso de references
esta disponible si usted está habilitado para usar la sintáxis hash en lugar de un string para sus condiciones where
, como en el siguiente ejemplo. Este automaticamente genera un LEFT OUTER JOIN
usando alias de nombres de tablas.
>> User.includes(:auctions)
.where(auctions: {name: 'Lumina'})
SQL (0.2ms) SELECT "users"."id" AS t0_r0, "users"."email" AS t0_r1,
"users"."password_digest" AS t0_r2, "users"."password_reset_token" AS
t0_r3, "users"."name" AS t0_r4, "users"."created_at" AS t0_r5,
"users"."updated_at" AS t0_r6, "users"."token" AS t0_r7,
"auctions"."id" AS t1_r0, "auctions"."name" AS t1_r1,
"auctions"."description" AS t1_r2, "auctions"."ends_at" AS t1_r3,
"auctions"."created_at" AS t1_r4, "auctions"."updated_at" AS t1_r5,
"auctions"."closes_at" AS t1_r6, "auctions"."user_id" AS t1_r7 FROM
"users" LEFT OUTER JOIN "auctions" ON "auctions"."user_id" =
"users"."id" WHERE "auctions"."name" = ? ["name", "Lumina"](/LPC-Ltda/Ruby-on-Rails/wiki/"name",-"Lumina")
Note que
includes
y su método hermano funcionan con nombres de asociaciones, mientras quereferences
necesitan los nombres de tabla actual.
joins(expression)
5.6.13 joins
trabaja en forma similar a includes
usando un INNER JOIN
en la consulta SQL resultante. Una de las partes clave del conocimiento que se debe comprender sobre los inner joins es que solo devuelven el conjunto de registros que coinciden con las tablas que se unen. Si a una fila en cualquier lado de la unión le falta su fila correspondiente en el otro lado, ninguna de las dos será devuelta en el conjunto de resultados.
>> User.joins(:auctions).to_sql
=> "SELECT users.* FROM users INNER JOIN auctions ON auctions.user_id = user_id"
La consulta en el ejemplo retorna no solo la fila en la tabla users que contiene la información del usuario, también trae de vuelta la filas correspondientes que pueblan la associación users.auctions
. Cuando esa asociación es accesada más tarde dentro del mismo ciclo de request, no significa consultas a la base de datos adicionales, así como ocurre con includes
.
El método joins
también entiende multiples joins, así como joins de asociaciones anidadas usando la notación hash.
>> User.joins(auctions: [:bids]).to_sql
=> "SELECT users.* FROM users INNER JOIN auctions ON auctions.user_id
= users.id INNER JOIN bids ON bids.auctions_id = auctions.id"
Mientras el método joins
normalmente toma los símbolos correspondientes a los nombres de las tablas y puede calcular la cláusula ON basándose en los metadatos de asociación, si desea proporcionar una expresión más compleja, escriba la cláusula usted mismo y pásela en un string.
Buyer.select(:*, 'count(carts.id) as cart_count')
.joins('left outer join carts on carts.buyer_id = buyers.id')
.group('buyers.id')
Por mucho el uso más común del método
joins
es cargar con ansias (eager) data para objetos asociados en una sentenciaSELECT
única con el fin de prevenir las llamada N+1 consultas. Note que las consultasLEFT JOIN
son tan populares que ellas tienen su propio métodoleft_outer_join
en Rails 5.
left_outer_join
5.6.14 Como mencioné previamente LEFT OUTER JOIN
es tan popular que tiene su propio método en Rails 5.
>> User.select(:*, 'count(bids.id) as bid_count)')
.left_outer_joins(auctions: [:bids])
.group('users.id').to_sql
=> "SELECT *, count(bids.id) as bid_count) FROM users LEFT OUTER JOIN auctions ON auctions.user_id = users.id LEFT OUTER JOIN bids ON bids.auction_id = auctions.id GROUP BY users.id"
EL método tiene el alias left_joins
para los que prefieren nombres cortos.
Sólo puedo imaginar usar
joins
yleft_outer_join
en circunstancias donde necesito el join solo para propósitos de consulta, y no para cargar ansiosamente una asociación, porque yo habría usadoincludes
a cambio. Esta parte de la API puede parecer demasiado confusa, pero pienso que está diseñada en forma de ayudarle a escribir el código más revelador de intenciones posible.
5.6.15 `find_or_create_by(attributes, &block)
find_or_create_by
encuentra el primer registro usando la relación y los attributes
dados, o si ninguno es encontrado, graba un registro nuevo usando los atrubutos provistos y los valores de la cláusula where de la relación.
Este ejemplo tonto mira por los usuarios activos llamados Buster y crea un registro que coincida si no se encuentra uno.
User.active.find_or_create_by(first_name: 'Buster', ...)
Asumiendo que el alcance active
es la cosa obvia, este código es idéntico al siguiente:
User.find_or_create_by(active: true, first_name: 'Buster', ...)
Si usted está jugando con esta técnica, valorará mirar create_with
el que le da la opción de especificar explicitamente el valor de los atributos usados para la creación (pero no para la consulta).
Note la pequeña diferencia de comportamiento entre este ejemplo y los dos anteriores.
>> User.create_with(active: true).find_or_create_by(first_name: 'Buster', ...)
Lo que hace este código es buscar cualquier usuario (activo o no), y si no lo encuentra, crea un usuario activo llamado Buster.
Y si por alguna razón usted lo necesita, usted puede pasar un block a find_or_create_by
. En el caso de generar un nuevo registro, se entregará al bloque antes de guardarlo en la base de datos. Esta técnica se verá como esto.
User.find_or_create_by(first_name: 'Scarlett') do |user|
user.last_name = 'Johansson'
end
Note que el comportamiento de find_or_create_by
no es atómico. Primero esto ejecuta un SELECT, y si no hay resultados, entonces se intenta un INSERT. Si hay otro threads o preceso, una carrera puede resultar en que dos registros similares sean guardados en la base de datos.
Como es usual en estas situaciones, si el registro que usted está tratando de crear tiene una restricción UNIQUE
a nivel de base de datos, entonces usted puede obtener la excepción resultante y retry
, como esto:
begin
CreditAccount.transaction(requires_new: true) do
CreditAccount.find_or_create_by(user_id: user.id)
end
rescue
retry
end
El find_or_create_by
es similar al create
regular en que si la validación if falla, no tratará de grabar en la base de datos y retornará un registro no grabado. No es sorprendente que también haya una versión de este método llamado find_or_create_by!
que (igual que create!
) levantará una excepción si la validación falla.
find_or_initialize_by(attributes, &block)
5.6.16 find_or_initialize_by
es muy similar a find_or_create_by
pero llama a new
en lugar de create
bajo la cubierta.
new(attributes, &block)
5.6.17 new
es muy similar a find_or_initialize_by
pero sin la operación find.
>> active_users = User.where(active: true)
>> active_users.new
=> #<User id: nil, active: true, created_at: nil, updated_at: nil>
create_with
5.6.18 Ver find_or_create_by
reload
5.6.19 Ver la sección "Recargando" en este capítulo.
reset
5.6.20 reset
elimina todas las configuraciones y el contenido de la relación. Se utiliza para garantizar que el siguiente acceso de una relación (si es necesario) llegue a la base de datos nuevamente en lugar de usar valores almacenados en caché. Compárelo con reload
, que siempre vuelve a golpear la base de datos sin importar qué.
explain
5.6.21 explain
ejecuta EXPLAIN
sobre la consulta o consultas gatilladas por esta relación y retorna el resultado como un string. Note que este método puede realmente ejecutar consultas como parte de su operación ya que el resultado es necesitado cuando una carga ansiosa está involucrada.
> User.includes(:auctions).where(auctions: {name: 'Foo'}).explain
SQL (0.2ms) SELECT "users"."id" AS t0_r0, ...
=> EXPLAIN for: SELECT "users"."id" AS t0_r0 ...
0|0|0| SCAN TABLE users
0|1|1| SEARCH TABLE auctions USING AUTOMATIC COVERING INDEX
(name=? AND user_id=?)
extending(*modules, &block)
5.6.22 extending
especifica uno o muchos módulos con métodos que extenderán el alcance con métodos adicionales. Retorna un objeto relation, para promover encadenamiento o extensión.
module Pagination
def page(number)
# pagination code
end
end
scope = Model.all.extending(Pagination)
scope.page(params[:page])
Usted puede pasar más de un módulo a extending
y esto también toma un block opcional (esencialmente actúa como un módulo anónimo).
# same example extend with a block
scope = Model.all.extending(Pagination) do
def per_page(number)
# pagination code goes here
end
end
exist?
5.6.23 exists
lleva los argumentos de un find
y en lugar de devolver registros devuelve un valor booleano para determinar si la consulta tiene resultados.
>> User.create(login: "mack")
=> #<User id: 1, login: "mack">
>> User.exists?(1)
=> true
>> User.exists?(login: "mack")
=> true
Por supuesto, este puede ser encadenado a una relación.
>> User.where(login: "mack").exists?
=> true
any?
5.6.24 any
es lo opuesto a empty
.
empty?
5.6.25 Si la relación es cargada, empty
retorna true
si ningún registro está presente. Si la relación no está cargada, hace un recuento bajo la cubierta para obtener un valor de retorno.
# File activerecord/lib/active_record/relation.rb
def empty?
return @records.empty? if loaded?
if limit_value == 0
true
else
c = clunt(:all)
c.respond_to?(:zero?) ? c.zero? : c.empty?
end
end
many?
5.6.26 many
retorna true
si la relación retorna más de un registro. Este es implementado usando SELECT COUNT
como se ve en el ejemplo.
>> User.where(id: 1).many?
(0.1ms) SELECT COUNT(*) FROM "users" WHERE "users"."id" = ? ["id", 1](/LPC-Ltda/Ruby-on-Rails/wiki/"id",-1)
=> false
one?
5.6.27 one?
retorna true
si la relación retorna exactamente un registro.
none
5.6.28 Introducido en Rails 4, ActiveRecord::QueryMethods.none
es una relación encadenable que causa que una consulta retorne cero registros. El método consulta retorna ActiveRecord::NullRelation
, lo cual es una implementación de pattern Null Object. Esto es usado en instancias donde usted tiene un método que retorna una relación, pero hay una condición en la cual usted no desea que la base de datos sea consultada. Todas las subsecuentes condiciones encadenadas trabajarán sin tema, eliminando la necesidad de chequear si el objeto con el que usted está trabajando es una relación.
def visible
case role
when :reviewer
Post.published
when :bad_user
Post.none
end
end
# If chained, the following code will not break for users
# with a :bad_user role
posts = currents_user.visible.where(name: params[:name])
lock
5.6.29 lock
especifica el seteo de bloqueo para una consulta. Esto es descrito anteriormente en este capítulo en la sección "Bloqueo de Base de datos".
readonly
5.6.30 Encadenar el método readonly
marca los objetos retornados como readonly
. Usted puede cambiar sus atributos, pero no podrá guardarlos de vuelta en la base de datos.
>> c = Comment.readonly.first
=> #<Comment id: 1, body: "Hey beeyotch!">
>> c.body = "Keep it clean!"
=> "Keep it clean"
>> c.save
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
reorder
5.6.31 Al usar reorder
usted puede reemplazar cualquier orden definido existente sobre una relación dada.
>> Member.order('name DESC').reorder(:id)
Member Load (0.6ms) SELECT "members".* FROM "members" ORDER BY
"members","id" ASC
Cualquier llamado subsecuente a order
será agregado a la consulta.
>> Member.order('name DESC').reorder(:id).order(:name)
Member Load (0.6ms) SELECT "members".* FROM "members" ORDER BY
"members".name ASC, "members"."id" ASC
reverse_order
5.6.32 reverse_order
es un método conveniente para reversar una cláusula de orden existente sobre una relación.
>> Member.order(:name).reverse_order
Member Load (0.6ms) SELECT "members".* FROM "members" ORDER BY
"members".name DESC
rewhere(conditions)
5.6.33 rewhere
habilta cambiar una condición where
previamente seteado para un atributo dado, en lugar de agregarla a la condición. (Rara vez se usa).
En la consola usted se puede asegurar que condiciones están vigentes en el scope o relación? Intente llamar a
where_values_hash
y te lo dirá.
scoping(&block)
5.6.34 scoping
define un scope para todas las consultas en el block provisto.
Comment.where(post_id: 1).scoping do
Comment.first
end
# SELECT * FROM comments WHERE comments.post_id = 1 LIMIT 1
unscope(*args)
5.6.35 El método de consulta unscope
es útil cuando usted desea remover una relación indeseada sin reconstruir la cadena de relación entera. Por ejemplo, para remover una cláusula order de una relación, agrege `unscope(:order)':
>> Member.order('name DESC').unscope(:order)
SELECT "members".* FROM "members"
Adicionalmente, uno puede pasar un hash como argumento para hacer un unscope
de valores :where
específicos. Esto causará que los valores especificados no sean incluidos en la cláusula where.
Member.where(name: "Tyrion", active: true).unscope(where: :name)
Es equivalente a
Member.where(active: true)
La siguiente es una lista de los métodos de consulta aceptados por unscope
:
:from
:group
:having
:includes
:joins
:limit
:lock
:offset
:order
:readonly
:select
:where
merge(other)
5.6.36 merge
mezcla en las condiciones desde una relación other
o arreglo. Si pasó un ActiveRecord::Relation
, entonces el valor retornado en una relación mezclada. Si pasó un arreglo, retorna un arreglo que representa la intersección de los registros resultanes con el arreglo other
.
Yo no puedo pensar inmediatamente en el uso de la intersección de arreglos usando este método, pero pude encontrar otra otra forma útil para componer código de consultas elegante.
# Find recent posts with comments highlighted by the editor
Post.recent.joins(:comments).merge(Comment.where(editor_pick: true))
Interesantemente, other
puede también ser un Proc
, cuyo contexto de evaluación es la relación en la que usted está mezclando. De acuerdo con la documentación esta es muy útil para las asociaciones.
# Find recent comments on a given post highlighted by the editor
editor_pick = -> { where(editor_pick: true) }
post.comments.latest.merge(editor_pick)
Sospecho que no hay una buena razón para hacer algo así como el último ejemplo, a menos que editor_pick
fuera un poco de lógica más compleja que hayas extraído en tu propio objeto y quieras compartir en diferentes contextos.
only(*onlies)
only
limita una relación a componentes específicos. Pase uno o más símbolos que representen la parte de la consulta a incluir.
# only keep the scope's where clause, discard anything else
Post.latest.only(:where)
except(*skips)
5.6.38 except
remueve parte de la consulta. Pase uno o más símbolos que representan la parte de la consulta que saltar. (Es muy raro necesitar hacer esto.)
# discard any order condition that might be on the scope
Post.latest.except(:order)
or(other)
5.6.39 Usted puede generar expresiones OR
en sus consultas SQL para encadenar nodos Arel juntos usando el método or
.
Member.where(name: "Tyrion").or(Member.where(family: 'Lannister').first)
# => SELECT * FROM members WHERE members.name = 'Tyrion' OR members.family = 'Lannister'
Compare esto con un AND lógico, el cual no requiere una llamada al método explicita y es el comportamiento por defecto del encadenamiento de Arel.
Member.where(name: "Tyrion").where(family: 'Lannister')
# => SELECT * FROM members WHERE members.name = 'Tyrion' AND members.family = 'Lannister'
load
5.6.40 load
carga la relación desde la base de datos y retorna la relación. Se usa en casos muy raros en los cuales se necesita cargar una relación durante su construcción. El valor retornado es la relación misma, no los registros.
to_a
5.6.41 to_a
carga la relación desde la base de datos y retorna el objeto Active Record resultante en un Array
(en lugar de envuelto en una relación).
to_sql
5.6.42 Como se muestra en numerosos ejemplos a lo largo de esta sección, llamar a to_sql
en una relación volcará el SQL generado. Es muy útil para la depuración de joins complicados.
to_json
y sus amigos
5.6.43 El resultado de relaciones puede ser serializado a una variedad de formatos textuales usando to_json
, to_yaml
, y to_xml
. Ellos usan la gema Psych en conjunción con encode_with(coder)
bajo la cubierta.
>> User.where(id: 1).to_json
User Load (0.1ms) SELECT users.* FROM users ...
=> "[{"id":1,"email":"[email protected]", ...}]"
arel_table
5.6.44 Para los casos en los cuales usted necesita generar SQL personalizado por usted mismo a través de Erel, usted puede usar el método arel_table
para obtener acceso a la instancia Table
para la clase.
>> users = User.arel_table
>> users.where(users[:login].eq("mack")).to_sql
=> "SELECT 'users'.'id', 'users'.'login' FROM 'users' WHERE 'users'.'login' = 'mack'"
Usted puede consultar la documentación de Arel directamente y cómo construir consultas personalizadas usando su DSL. (https://github.com/rails/arel/)
cache_key
6.6.45 cache_key
retorna la llave cache que puede ser usada para identificar el registro obtenido por esta consulta.
>> Product.where("name like ?", "%Cosmic Encounter%").cache_key
=> "products/query-1850ab3d302391b85-1-20150714212553907087000"
Una descripción completa de este método y cómo usarlo es presentada en el Capitulo 17, "Caching y Desempeño."
Ignorando Columnas
##5.7 Conexión a múltiples bases de datos en diferentes modelos
Las conexiones son creadas vía ActiveRecord::Base.establish_connection
y recuperadas por ActiveRecord::Base.connection
. Todas las clases heredadas de ActiveRecord::Base
usarán esta conexión. Que pasa si usted quiere que alguno de sus modelos use una conexión diferente? Usted puede agregar conexiónes de una clase específica.
Por ejemplo, digamos que usted necesita acceder a data que reside en una base de datos heredada diferente a la base de datos que usted usa para el resto de su aplicación Rails. Crearemos una nueva clase base que pueda ser usada por modelos que accesan la base de datos heredada. Comenzando por agregar los detalles de la base de datos adicional bajo su propia clave en database.yml
. Luego llamar a establish_connection
para hacer que LegacyProjectBase
y todas sus subclases usen la conexión alternativa.
class LegacyProjectBase < ActiveRecord::Base
establish_connection :legacy_database
self.abstract_class = true
...
end
Incidentalmente, para hacer que este ejemplo trabaje con subclases, usted debe especificar self.abstract_class = true
en el contexto de la clase. De otra forma, Rails considera que las subclases de LegacyProject
usarán herencia de tabla única (STI), lo cual discutiremos en extenso en el Capítulo 9 "Active Record Avanzado".
Usted puede fácilmente apuntar su clase base a diferentes bases de datos dependiendo del ambiente de Rails:
class LagacyProjectBase < ActiveRecord::Base
establish_connection "legacy_#{Rails.env}"
self.abstract_class = true
...
end
Entonces sólo agregue múltiples entradas a database.yml
para hacer coincidir los distintos nombres de conexión -en el caso de nuestro ejemplo, legacy_development
, legacy_test
, etc.
El método establish_connection
toma una llave string (o símbolo) apuntando a la configuración ya definida en database.yml
Alternativamente, usted puede pasarle un hash de opciones literales, aunque es desordenado poner este tipo data de configuración dentro de su archivo modelo en lugar de en database.yml
.
class TempProject < ActiveRecord::Base
establish_connection adapter: 'sqlite3', database: ':memory:'
...
end
Rails pone las conexiones a las base de datos en un pool de conexiones dentro de la instancia de la clase ActiveRecord::Base. El pool de conexiones es simplemente un objeto Hash
indexado por la clase Active Record. Durante la ejecución, cuando se necesita una conexión, el método retrieve_connection
caminará hacia arriba en la jerarquia de clases hasta que una conexión coincidente sea encontrada.
##5.8 Usando la conexión a base de datos directamente
Es posible usar la conexión a la base de datos subyacente de Active Record directamente, y algunas veces es útil hacerlo desde scripts personalizados y para pruebas one-off o ad hoc. Acceder a la conexión vía el atributo connection
de cualquier clase Active Record. Si todos sus modelos usan la misma conexión, entonces use el atributo connection de ActiveRecord::Base
.
ActiveRecord::Base
ActiveRecord::Base.connection.execute("show tables").values
La operación más básica que usted puede hacer con una conexión es simplemente que execute
una instrucción SQL desde el módulo DatabaseStatements
. Por ejemplo, El Listado 5.1 muestra un método que ejecuta un archivo SQL instrucción por instrucción.
Listado 5.1 Ejecuta un archivo SQL línea a línea usando la conexión de Active Record
def execute_sql_file(path)
File.read(path).split(';').each do |sql|
begin
ActiveRecord::Base.connection.execute(#{sql}\n") unless sql.blank?
rescue
$stderr.puts "warning: #{$!}"
end
end
end
###5.8.1 El módulo DatabaseStatements
El módulo ActiveRecord::ConnectionAdapters::DatabaseStatements
mezcla un número de métodos útiles dentro del objeto connection que hacen posible trabajar con la base de datos directamente en lugar de usar los modelos de Active Record. Hemos obviado a propósito algunos de estos métodos porque son usados internamente por Rails para construir instrucciones SQL dinámicamente, y pienso que no tienen mucha aplicación para desarrolladores.
Para facilitar la lectura en los siguientes ejemplos select_
, asuma que el objeto connection ha sido asignado a conn
:
conn = ActiveRecord::Base.connection
5.8.1.1 begin_db_transaction()
Comienza una transacción de base de datos manualmente (y apaga el comportamiento autocommit de Active Record).
5.8.1.2 commit_db_transaction()
Hace el commit de la transacción (y enciende el comportamiento auto-commit de Active Record nuevamente).
5.8.1.3 delete(sql_atatement)
Ejecuta una instrucción SQL DELETE provista y retorna el número de filas afectadas.
5.8.1.4 execute(sql_statement)
Ejecuta una instrucción SQL provista en el contexto de esta conexión. Este método es abstracto en el módulo DatabaseStatements
y es sobre-escrito por implementaciones de adaptadores de bases de datos específicos. De esta forma el tipo resultado es un conjunto de objetos resultado correspondiente al adaptador en uso.
5.8.1.5 insert(sql_statement)
Ejecuta una instrucción SQL INSERT y retorna el último id autogenerado desde la tabla afectada.
5.8.1.6 `reset_secuence!(table, column, sequence = nil)
Usado en Oracle y Postgres; actualiza la secuencia nombrada al máximo valor de la columna de la tabla especificada.
5.8.1.7 rollback_db_transaction()
Hace el roll back de la transacción activa actualmente (y enciende el auto-commit). Es llamada automáticamente cuando un block de transacción levanta una excepción o retorna false.
5.8.1.8 select_all(sql_statement)
Retorna una arreglo de hashes de registros con el nombre de columna como llave y el valor de la columna como valor.
conn.select_all("select name from businesses limit 5")
=> [{"name"=>"Hopkins Painting"}, {"name"=> "Whelan & Scherr"},
{"name"=>"American Top Security Svc"}, {"name"=>"Life Style Homes"},
{"name"=>"378 Liquor Wine & Beer"}]
5.8.1.9 select_one(sql_statement)
Trabaja similarmente a select_all
pero retorna sólo la primera fila del conjunto resultado setado como un hash único con los nombres de las columnas como llaves y los valores de las columnas como valores. Note que este método no agrega una cláusula límit a su instrucción SQL automáticamente, así que considere agregar una para consultas sobre conjuntos de datos grandes.
>> conn.select_one("select name from businesses")
=> {"name"=>"new York New York Salon"}
5.8.1.10 select_value(sql_statement)
Trabaja como select_one
, excepto que retorna un valor único: el valor de la primera columna de la primera fila del conjunto resultado.
>> conn.select_value("select * from bussinesses limit 1")
=> "Cimino's Pizza"
5.8.1.11 select_values(sql_statement)`
Trabaja como select_value
, excepto que retorna un arreglo de los valores de la primera columna en todas las filas del conjunto resultado.
>> conn.select_values("select * from businesses limit 5")
=> ["Ottersberg Christine E Dds", "Bally Total Fitness", "Behboodikah,
Mahnaz Md", "Preferred Personnel Solutions", "Throughbred Carpets"]
5.8.1.12 update(sql_statement)
Ejecuta la instrucción update provista y retorna el número de filas afectadas. Trabaja exactamente como delete
###5.8.2 Otros métodos de conexión
La lista completa de métodos disponibles para connection
que retornan una instancia del adaptador de base de datos subyacente es enorme. La mayoría de las implemetaciones de adaptadores Rails definen propias versiones personalizadas de estos métodos. Esto hace sentido, ya que todas las bases de datos tienen pequeñas variaciones en el cómo ellas manejan SQL y grandes variaciones en el cómo manejan los comandos extendidos, como por ejemplo obtener metadata.
Una ojeada a abstract_adapter.rb
nos muestra las implementaciones de métodos por defecto:
...
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
'Abstract'
end
# Does this adapter support migrations? Backend specifics, as the
# abstract adapter always return +false+.
def supports_migrations?
false
end
# Can this adapter determine the primary key for tables not attached
# to an Active Record class, such as join tables? Backend specific, as
def support_primary_key?
false
end
...
En la siguiente lista de descripción de métodos y códigos ejemplo, Accesaré la conexión de nuestra aplicación de muestra time_and_expenses
en la consola Rails, y nuevamente, asignaré connection
a la variable local llamada conn
.
5.8.2.1 active?
Indica cuando la conexión está activa y lista para realizar consultas.
5.8.2.2 adapter_name
Retorna el nombre legible para humanos del adaptador:
>> conn.adapter_name
=> "SQLite"
5.8.2.3 disconnect!
y reconnect!
Cierra la conexión activa o cierra y abre una nueva en su lugar, respectivamente.
5.8.2.4 raw_connection
Provee acceso a la conexión de base de datos subyacente. Útil para cuando usted necesita ejecutar una instrucción propietaria o usted está usando características del driver de base de datos Ruby que no están necesariamente expuestas en Active Record. (Al tratar de llegar a un ejemplo de código de este método, yo fui capaz de estrellar la consola de Rails con facilidad. No hay muchas formas de chequear errores por excepciones que pueda levantar mientras trabaja con raw_connection
.)
5.8.2.5 supports_count_distinct?
Indica si el adaptador soporta el uso de DISTINCT con COUNT en una instrucción SQL. Este es true para todos los adaptadores excepto SQLite, el cual requiere trabajo adicional cuando hace operaciones como cálculo.
5.8.2.6 supports_migrations?
Indica si el adaptador soporta migraciones.
5.8.2.7 tables
Produce una lista de tables en el schema de base de datos subyacente. Esta incluye tablas que no son usualmente expuestas como modelos Active Record, como schema_info
y sessions
.
>> conn.tables
=> ["schema_migrations", "users", "timesheets", "expense_reports",
"billable_weeks", "clients", "billing_codes", "sessions"]
**5.8.2.8 verify!(timeout)
Perezosamente verifica esta conexión, llamando a active?
solo si no ha sido llamado por timeout
segundos.
##5.9 Otras opciones de configuración
Adicionalmente a las opciones de configuración usadas para dar instrucciones a Active Record acerca del cómo manejar el nombre de las tablas y llaves primarias, hay un número de otros seteos que gobiernan funciones miscelaneas. Seteelas en un inicializador.
5.9.0.1 ActiveRecord::Base.default_timezone
Le cuenta a Rails si usar Time.local
(usando :local
) o Time.utc
(usando :utc
) cuando sacan fechas y horas desde la base de datos. Por defecto es :local.172
.
5.9.0.2 ActiveRecord::Base.schema_format
Especifica el formato a utilizar cuando se vacía el schema de base de datos con ciertas tareas rake por defecto. Use la opción :sql
para tener el schema vaciado como instrucciones SQL potencialemente específicas de base de datos. Sólo tenga cuidado de las incompatibilidades si usted está tratando de usar la opción :sql
con diferentes bases de datos para development y testing. La opción por defecto es :Ruby
. lo cual vacía el schema como un archivo ActiveRecord::Schema
que puede ser cargado dentro de cualquier base de datos que soporta migraciones.
5.9.0.3 ActiveRecord::Base.store_full_sti_class
Especifica si Active Record almacenará los nombres de constantes completos incluyendo namespace cuando use herencia de tabla única (STI), cubierto en el Capítulo 9, "Active Record Avanzado".