Modelos de datos avanzados con Rails
() translation by (you can also view the original English article)
Modelo, Vista, Controlador. Si alguna vez has probado Ruby on Rails, es probable que esas palabras hayan sido introducidas mil veces en tu cabeza. Por otro lado, si esto es nuevo para usted, hay muchos recursos en Nettuts+ para principiantes, pero este no es uno de ellos.
Si has llegado hasta aquí, es seguro asumir que eres al menos un usuario intermedio de Rails. Si es así, te felicito! La curva de aprendizaje más difícil está detrás de ti. Ahora, puedes comenzar a dominar las cosas realmente geniales. En este post, quiero centrarme en mi tercero favorito de MVC, el que creo que Rails hace mejor: los modelos.
Orientación a objetos
Con mucho, lo mejor de ActiveRecord (en mi opinión) es cómo las filas en una base de datos se corresponden directamente con las clases en su código. ¡Pero no cometa el error de suponer que sus clases de modelo son las filas de la base de datos! Más bien, debe darse cuenta de que las clases de modelo son simplemente una forma de interactuar con la base de datos. La parte brillante es que Rails hace que este proceso sea tan sin fricción que podría pensar que está manipulando la información directamente. No cometas este error.
La clave para dominar los modelos es usarlos como clases "regulares", con métodos de instancia, métodos de clase, atributos y similares. Debe tratar las columnas en la base de datos como partes de su modelo, accediendo a ellas desde el exterior, pero también usándolas desde dentro de otras construcciones como métodos privados y de instancia. Aprende a integrar los dos para un sistema verdaderamente orientado a objetos.
Todo lo que hace la base de datos es proporcionar la información. Tú eres quien debe proporcionar la lógica.
Piense en los objetos del modelo como cualquier otro objeto; si hay lógica en otra parte de su código que se relaciona con este objeto, siempre es mejor integrarlo dentro de la clase. Todo lo que hace la base de datos es proporcionar la información. Tú eres el que debe proporcionar la lógica.
Métodos
Digamos que tienes un modelo que corresponde a una publicación en un blog. Decides escribir todas tus publicaciones de blog con Markdown y almacenar solo las versiones de Markdown en la base de datos. Ahora, debe analizar este contenido en HTML cada vez que desee mostrarlo.
1 |
|
2 |
<article>
|
3 |
<h1><%= @post.title %<</h1> |
4 |
|
5 |
<%= format_markdown(@post.content) # Just an example, not a real method %> |
6 |
</article>
|
Este archivo de vista básica ejecutaría el contenido de la publicación a través de un método cada vez que se muestra. Bueno. ¿Cómo podemos mejorarlo?
1 |
|
2 |
# ./app/models/post.rb
|
3 |
|
4 |
class Post < ActiveRecord::Base |
5 |
|
6 |
...
|
7 |
|
8 |
def formatted_content |
9 |
format_markdown(self.content) |
10 |
end
|
11 |
|
12 |
end
|
1 |
|
2 |
<article>
|
3 |
<h1><%= @post.title %<</h1> |
4 |
|
5 |
<%= @post.formatted_content # much better! %> |
6 |
</article>
|
He consolidado el formato del contenido en un método de instancia. Si bien esto aún no tiene ningún efecto notable, imagínese si más tarde decidiera agregar más lógica al formato, por ejemplo, verifique si el contenido ya está en formato HTML. De esta manera sería mucho más fácil adaptar el código más adelante.
También tiene el beneficio adicional de hacer que su código sea más organizado y más fácil de leer. Si estuviera viendo tu proyecto, el primer lugar en el que buscaría un método como este sería en tu clase Post. Este tipo de organización imita directamente el tipo ya incorporado en Rails: métodos pequeños y ligeros adjuntos a las clases.
Métodos de clase
Los métodos de clase le permiten consolidar métodos relacionados con una funcionalidad específica en una ubicación consistente. Realmente no es una idea loca. De hecho, como desarrollador intermedio de Ruby, es uno con el que deberías estar muy cómodo. Muchos no se dan cuenta de que también puede crear métodos de clase en un modelo, pero los modelos son clases como cualquier otra, ¿por qué no deberíamos hacerlo?
En lugar de poner los métodos de ayuda en un archivo de ayuda separado, simplemente agréguelos al Modelo.
Un ejemplo perfecto de esto son los métodos de ayuda o el código de ayuda agrupados por su propósito. En lugar de colocarlos en un archivo / módulo de ayuda separado (que solo debe usarse para los controladores y los asistentes de visualización), simplemente agréguelos al Modelo. Por ejemplo, si deseaba un método que validara la autenticidad de un nombre de usuario y una contraseña dados, podría implementar un método User.authenticate()
. Ahora, si más adelante desea modificar la función de autenticación, será fácil de encontrar entre el resto del código relacionado con el Usuario.
Ese último es un punto importante. Uno de los mantras de Rails es "Convención sobre configuración". Si todos los proyectos se adhieren a una estructura de código y archivo similar, es más fácil para otro desarrollador ingresar y comenzar a realizar cambios en un proyecto. Una de estas convenciones es agrupar los métodos de ayuda relacionados con el modelo como métodos de clase.
Los métodos de ayuda que solo se usan internamente en el Modelo también pueden existir como métodos de clase. En este caso serían métodos privados o protegidos, pero la idea básica es la misma. Si el método necesita información específica para una instancia, es un método de instancia privada. Si no, es un método de clase. Es tan simple como eso, de verdad.
Atributos Virtuales
Un modelo no es el registro en la base de datos. Se crean no cuando los datos se escriben en la base de datos, sino cuando usted, el programador, los inicializa.
Para entender correctamente a los modelos, debes entender su estilo de vida del objeto. Como dije antes, un Modelo no es el registro en la base de datos. Se crean no cuando los datos se escriben en la base de datos, sino cuando usted, el programador, los inicializa. Dado que el Modelo es solo una interfaz para los datos en bruto de la base de datos, puede crear un nuevo objeto de la clase para que se corresponda con los datos existentes.
Del mismo modo, se pueden eliminar llamando a su método de destrucción. Esto elimina la fila de la base de datos y también elimina el objeto Modelo.
Pero puede eliminar el objeto Modelo (¡no la fila de la base de datos!) Estableciendo la variable en nil, o simplemente dejándola fuera del alcance.
Una vez que comprenda esto, es trivial darse cuenta de que un Modelo puede tener atributos que no corresponden a una columna de base de datos. Piense en un modelo de usuario. Este usuario tiene un nombre de usuario y contraseña, pero debido a que usted es un programador preocupado por la seguridad, solo desea almacenar una contraseña con hash en la base de datos, no la contraseña de texto.
Para lograr esto, puede implementar un "atributo virtual" o una variable de instancia que no sea una columna en la base de datos. Para mantener las cosas simples, el usuario no tendrá que hacer ningún tipo de confirmación de contraseña.
1 |
|
2 |
# ./app/models/user.rb
|
3 |
|
4 |
class User < ActiveRecord::Base |
5 |
|
6 |
attr_accessor :password |
7 |
|
8 |
# database has encrypted_password column
|
9 |
|
10 |
...
|
11 |
|
12 |
end
|
Este es un constructo Ruby simple y puro que agrega métodos getter y setter a un objeto. Ahora puede cifrar la contraseña antes de guardarla en la base de datos, por lo que la contraseña de texto sin formato solo se almacena en la memoria, no se escribe en ningún archivo.
1 |
|
2 |
user = User.new(params[:user]) |
3 |
user.password = params[:user][:password] # redundant, since the above call does this already |
4 |
user.encrypt_password # sets the encrypted password attribute to the virtual attribute encrypted |
5 |
user.save! |
Si bien esto dista mucho de ser una implementación perfecta (te mostraré cómo mejorarlo más adelante en la publicación), demuestra el poder de los atributos virtuales. Ahora puede implementar el método encrypt_password
de la forma que desee, probablemente con la contraseña almacenada en la memoria junto con algún tipo de sal basado en la fecha actual o algo así. En este caso, es importante llamar a ese método antes de guardar al usuario, para que se genere la contraseña hash
Alcances
Considera esto:
1 |
|
2 |
Post.where("created_at > ?", 1.week.ago).order("created_at ASC") |
Estos objetos contienen toda la información necesaria para obtener datos de la base de datos. El objeto responde a los métodos anteriores, por lo que puede construir incrementalmente una consulta de base de datos encadenando los métodos.
Suficientemente fácil. Solo estamos creando una consulta de base de datos mediante el encadenamiento de métodos. ¿Alguna vez has pensado, sin embargo, de dónde vienen esos métodos? En realidad es bastante simple. Ciertos métodos como .where()
, .order()
y .select()
se utilizan para generar consultas. Lo hacen mediante todos los objetos que retornan de la clase ActiveRecord::Relation. Estos objetos contienen toda la información necesaria para obtener datos de la base de datos. El objeto responde a los métodos anteriores, por lo que puede construir incrementalmente una consulta de base de datos encadenando los métodos.
Estos métodos se denominan ámbitos. Como puedes imaginar, son una herramienta increíblemente poderosa, hecha doblemente por el hecho de que puedes crear ámbitos propios.
Imagina otra vez nuestro Post Modelo. Para mostrar publicaciones, a menudo desea que aparezcan en el orden en que se crearon. Sin embargo, se vuelve engorroso agregar .order("created_at ASC")
a cada consulta que escriba. Podrías implementarlo en un método de clase de Post, como este:
1 |
|
2 |
# ./app/models/post.rb
|
3 |
|
4 |
class Post < ActiveRecord::Base |
5 |
|
6 |
...
|
7 |
|
8 |
def self.chronological |
9 |
self.order("created_at ASC") |
10 |
end
|
11 |
|
12 |
end
|
A primera vista, esto parece una buena solución. Hemos incluido código relevante en nuestro objeto, como los buenos desarrolladores orientados a objetos que somos, y cuál es el problema. El problema es que si intentas crear una consulta como antes:
1 |
|
2 |
Post.where("created_at > ?", 1.week.ago).chronological |
Como solo Post
tiene este método, verás un error como este:
1 |
|
2 |
NoMethodError: undefined method `chronological' for #<ActiveRecord::Relation:0x000001011324d0> |
Entonces, ¿qué tiene que hacer un chico?. Sencillo.
1 |
|
2 |
# ./app/models/post.rb
|
3 |
|
4 |
class Post < ActiveRecord::Base |
5 |
|
6 |
scope :chronological, : order => 'created_at ASC' |
7 |
|
8 |
...
|
9 |
|
10 |
end
|
¡Genial! Ahora, el ejemplo de arriba funciona perfectamente, y aún tenemos el código en el lugar correcto, en nuestro Modelo de Post.
Los ámbitos son muy flexibles. Si quisieras, podrías escribir el ejemplo anterior así:
1 |
|
2 |
scope :chronological, order('created_at ASC') |
El segundo argumento del método scope
puede ser un hash (como el primer ejemplo), un objeto ActiveRecord::Relation (como el segundo ejemplo), o incluso una lambda (función anónima) como esta:
1 |
|
2 |
scope :chronological, lambda { order('created_at ASC') } |
Con esta técnica, incluso podría pasar un argumento a .chronological
. Tal vez un booleano para ordenar ascendente o descendente?
1 |
|
2 |
scope :chronological, lambda { |ascending| ascending ? order('created_at ASC') : order('created_at DESC') } |
Y llama al método así:
1 |
|
2 |
Post.where("created_at > ?", 1.week.ago).chronological(false) |
Aunque puedes hacer más que solo ordenar.
1 |
|
2 |
scope :chronological, : order => 'created_at ASC', :where => 'published = true' |
1 |
|
2 |
scope :chronological, where(:published => true).order('created_at ASC') |
1 |
|
2 |
scope :chronological, lambda { |ascending| where(:published => true).order("created_at #{ascending ? 'ASC' : 'DESC'}") } |
Lo último que quiero mostrarle sobre los ámbitos es que puede
proporcionar lo que se denomina default_scope
. Este es un ámbito que se aplicará a todas las consultas basadas en este Modelo. Si solo quisieras mostrarlos en orden cronológico, podrías hacer:
1 |
|
2 |
# ./app/models/post.rb
|
3 |
|
4 |
class Post < ActiveRecord::Base |
5 |
|
6 |
default_scope order("created_at ASC") |
7 |
|
8 |
...
|
9 |
|
10 |
end
|
¿No es esto divertido?
Devoluciones de llamada
Si recuerda el ejemplo de cifrado de mi contraseña anterior, recordará que mencioné que se podría mejorar la forma en que llamamos el método encrypt_password
. Aquí está el fragmento original para referencia:
1 |
|
2 |
user = User.new(params[:user]) |
3 |
user.password = params[:user][:password] |
4 |
user.encrypt_password |
5 |
user.save! |
Como puede ver, si ciframos la contraseña de esta manera, tendremos que llamar a ese método de cifrado cada vez que iniciemos un nuevo Usuario. Si olvidamos llamarlo, el usuario no tendrá una contraseña con hash. Lejos de una solución ideal.
Afortunadamente, a la manera típica de Rails, hay una forma sencilla de solucionarlo.
Podemos usar lo que se llama una devolución de llamada, o una pieza de código que se llama cuando se alcanza una determinada fase del ciclo de vida de un objeto. Por ejemplo:
- Antes de guardar
- Despues de guardar
- Antes de la validación
- Despues de validacion
1 |
|
2 |
# ./app/models/user.rb
|
3 |
|
4 |
class User < ActiveRecord::Base |
5 |
|
6 |
...
|
7 |
|
8 |
before_save :encrypt_password |
9 |
|
10 |
def encrypt_password |
11 |
# do something
|
12 |
end
|
13 |
|
14 |
end
|
Es realmente tan simple como eso. Cuando un objeto de usuario está a punto de guardarse, se llama a un método. De hecho, ¡ni siquiera tienes que usar un método!
1 |
|
2 |
# ./app/models/user.rb
|
3 |
|
4 |
class User < ActiveRecord::Base |
5 |
|
6 |
...
|
7 |
|
8 |
before_save do |
9 |
# do something
|
10 |
end
|
11 |
|
12 |
end
|
En lugar de pasar un símbolo a la llamada before_save
, solo puede asignarle un bloque de código. Esto es ideal para situaciones en las que el código al que está llamando solo tiene una línea o dos, o si solo se usa antes de guardar el registro.
La lista completa de métodos de devolución de llamada está disponible en la documentación de la API. Incluyen unos como before_validation
, after_validation
, after_save
, y otros. Hay muchos de ellos, pero todos funcionan de forma muy parecida a lo que usted esperaría después de verlos antes: before_save
en acción.
La información anterior habla por sí misma, así que mantendré este breve. Esperemos que hayas aprendido algo, tal vez incluso algunas cosas. Si ya supieras esto, ¿qué consejos darías a otros sobre los modelos de Rails? ¡Publícalos en comentarios y continúa el aprendizaje!