07. ORM & ActiveRecord - williamromero/curso-rails GitHub Wiki
Guía de Referencia para los Modelos
Annotate Gem
La gema annotate no permite tener descrita la estructura del modelo dentro del mismo. Esto nos permite conocer los campos que existen en nuestra tabla. Esta gema normalmente se instala en el ambiente de desarrollo, puesto que no tiene ninguna utilidad en producción.
# Gemfile
gem 'annotate'
# En consola
bundle install # Para cargar la nueva gema, corremos el siguiente comando
rails g annotate:install # Corremos este comando para automatizar que la gema annotate actualice los cambios de los atributos cada vez que haya una migración en la base de datos.
# => create lib/tasks/auto_annotate_models.rake
Creando el modelo de Productos
rails g model Product name:string description:string stock:integer 'price:decimal{10,2}'
Annotate Gem :: Problemas en creación del esquema
Si al momento de correr la migración de la tabla Productos, el esquema no se agrega al modelo, es necesario correr el siguiente comando para que actualice o agregue los atributos del mismo:
annotate --models
# => Annotated (3): app/models/product.rb, spec/models/product_spec.rb, spec/factories/products.rb
Revisar el estado de las Migraciones
rails db:migrate:status
Status Migration ID Migration Name
-----------------------------------------
down 20220406062607 Create products
Si vamos al documento de la migración, podremos ver la estructura de la tabla que vamos a crear:
class CreateProducts < ActiveRecord::Migration[6.1]
def change
create_table :products do |t|
t.string :name
t.string :description
t.integer :stock
t.decimal :price, precision: 10, scale: 2
t.timestamps
end
end
end
Luego de ver y validar si los campos que vamos a crear están de la forma que deseamos, corremos el comando rails db:migrate y se creará nuestro modelo Producto y a su vez, en el mismo habrá la siguiente descripción:
rails db:migrate
Y luego, si revisamos el documento, podremos ver algo por el estilo:
# app/models/product.rb
# == Schema Information
#
# Table name: products
#
# id :bigint not null, primary key
# description :string(255)
# name :string(255)
# price :decimal(10, 2)
# stock :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class Product < ApplicationRecord
end
RAILS CONSOLE
La consola de Rails nos permitirá entre muchas cosas, poder interactuar con la información, objetos y clases que Rails nos provee. En este caso, nos servirá para poder interactuar con los modelos.
rails console
Product.connection
product = Product.new
#=> <Product:0x00007fd858f8bb18
product.attributes
=> {"id"=>nil, "name"=>nil, "description"=>nil, "stock"=>nil, "price"=>nil, "created_at"=>nil, "updated_at"=>nil}
product.methods
product.attributes.except!("price")
=> {"id"=>nil, "name"=>nil, "description"=>nil, "stock"=>nil, "created_at"=>nil, "updated_at"=>nil}
Crear objetos Product desde la consola de Rails:
Using save command:
laptop = Product.new
# => #<Product:0x00007f9df720f5e8 id: nil, title: nil, code: nil, stock: 0, price: 0, uuid: nil, created_at: nil,updated_at: nil>
laptop.title = 'HP Pavilion Touch' #=> "HP Pavilion Touch"
laptop.code = '0001' # => "0001"
laptop.price = 75000 # => 75000
laptop.stock = 10 # => 10
laptop.save
# (0.1ms) BEGIN Product Create (0.5ms) INSERT INTO `products` (`title`, `code`, `stock`, `price`,`uuid`, `created_at`, `updated_at` VALUES ('HP Pavilion Touch', '0001', 10, 75000, NULL, '2022-04-06 07:14:42.297022', '2022-04-06 07:14:42 297022')
# TRANSACTION (0.8ms) COMMIT => true monitor = Product.new(title: 'Dell Screen 27"', code: '0003', price: 50000, stock: 50)
monitor.save
monitor.persisted? # => true
monitor.new_record? # => true
monitor.update(stock: 25)
monitor.previous_changes # => { "stock" => [50, 25] }
mouse = Product.create(title: 'Logitec Gaming L', price: 25000, stock: 200, code: '0004')
# TRANSACTION (0.1ms) BEGIN Product Create (0.3ms) INSERT INTO `products` (`title`, `code`, `stock`, `price`, `uuid`, `created_at`, `updated_at`) VALUES ('Logitec Gaming L', '0004', 200, 25000, 'd5cd8094-b6dc-4734-be94-51a751d69b70', '2022-04-06 07:51:39.515536', '2022-04-06 07:51:39.515536')
# TRANSACTION (1.2ms) COMMIT => #<Product:0x00007fb127f8c430
Callbacks en ActiveRecord
Los callbacks en Rails, son funciones que podemos ejecutar durante el proceso de persistencia, actualización o eliminación de un registro de nuestra base de datos. Estas funciones, nos permiten ejecutar un método el cual hará la tarea que se le requiera, en el momento en el que se le necesite. Los callbacks, son tareas que cuentan con un prefijo, el cual denota la temporalidad en el que deberá de ejecutarse.
- Before
- Around
- After
A continuación, encontrará listados los callbacks que existen y con sus prefijos:
| Before | Around | After |
|---|---|---|
| CREANDO UN OBJECTO | ||
| before_validation | after_validation | |
| before_save | around_save | after_save |
| before_create | around_create | after_create |
| after_commit | ||
| after_rollback | ||
| EDITANDO UN OBJETO | ||
| before_validation | after_validation | |
| before_save | around_save | after_save |
| before_update | around_update | after_update |
| after_commit | ||
| after_rollback | ||
| DESTRUYENDO UN OBJETO | ||
| before_destroy | around_destroy | after_destroy |
| after_commit | ||
| after_rollback |
A continuación, un ejemplo del uso de los callbacks:
# app/models/product.rb
class Product < ApplicationRecord
# callbacks
before_save :notify_product_saving
after_save :notify_product_saved
def notify_product_saving
puts " - Producto #{self.name} guardado - "
end
def notify_product_saved
puts " - Producto #{self.name} persistido y almacenado en bodega - "
end
end
En consola:
product = Product.last
product.price = 7.00
product.save
- Producto Galleta Oreo guardado -
TRANSACTION (3.6ms) BEGIN
Product Update (1.3ms) UPDATE `products` SET `products`.`price` = 7.0, `products`.`updated_at` = '2022-05-31 02:26:44.189611' WHERE `products`.`id` = 4
- Producto Galleta Oreo persistido y almacenado en bodega -
TRANSACTION (44.1ms) COMMIT
=> true
Callbacks & Condiciones
Tal cual hemos conocido ya el uso de los callbacks de forma inicial, podremos conocer a continuación la interacción entre un callback y una condición. Esta condición, validará los valores de un campo y si es el resultado booleano es true, se ejecutará el callback y el método vinculado al mismo.
# app/models/product.rb
before_update :out_of_stock, if: :stock_changed?
def stock_changed?
self.stock_was != self.stock && self.stock < 5
end
def out_of_stock
puts " - Producto #{self.name} con stock reducido, llamar al proveedor - "
end
Después de esto, en rails console:
product = Product.new name: 'Conector de Red', description: 'Conector de cable de red para router', price: 10.00, stock: 20
product.save
# - Producto Conector de Red guardado -
TRANSACTION (0.2ms) BEGIN
Product Create (0.4ms) INSERT INTO `products` (`name`, `description`, `stock`, `price`, `created_at`, `updated_at`) VALUES ('Conector de Red', 'Conector de cable de red para router', 20, 10.0, '2022-05-31 02:40:51.615450', '2022-05-31 02:40:51.615450')
# - Producto Conector de Red persistido y almacenado en bodega -
TRANSACTION (1.5ms) COMMIT
# ACTUALIZANDO REGISTRO
product.stock = 4
product.save
# - Producto Conector de Red guardado -
# - Producto Conector de Red con stock reducido, llamar al proveedor -
TRANSACTION (0.2ms) BEGIN
Product Update (1.2ms) UPDATE `products` SET `products`.`stock` = 4, `products`.`updated_at` = '2022-05-31 02:43:44.313886' WHERE `products`.`id` = 7
# - Producto Conector de Red persistido y almacenado en bodega -
TRANSACTION (1.4ms) COMMIT
VALIDACIONES
Las validaciones nos permiten prever escenarios y establecer reglas relativas a los atributos de los objetos que manipulamos mediante el ActiveRecord y otros módulos de Rails. Estas reglas, nos permiten validar de forma previa a que los objetos se persistan o sean actualizados, ya que sin una correcta normalización de la información en una base de datos, tendremos recurrentemente problemas por información que no vendrá como la requerimos de la base de datos.
Existen 3 tipos de validaciones las cuales se utilizan para diferentes escenarios. A continuación, haremos algunas pruebas que explicarán el uso y razón de cada una de ellas.
### VALIDACIONES ESENCIALES Proveniente del módulo ActiveModel::Validations, es una librería que contiene varios métodos de clase que pueden emplearse para realizar validaciones cortas y sencillas. Estas provienen de los módulos internos llamados ActiveModel::Dirty y también del ActiveModel::Model y nos sirven para revisar si el objeto de clase que hemos creado posee una estructura válida, si se ha cambiado un valor del objeto o si se ha persistido en la tabla que representa al objeto. La ejecución de los métodos valid?, changed? o persisted? dependerán de validaciones que se hayan agregado previamente a nuestro modelo. A continuación, un ejemplo.
# app/models/product.rb
validates :description, presence: { message: 'El campo descripción no ha sido correctamente introducido' }
product = Product.new name: 'Galletas Gama', stock: 5, price: 10
# => #<Product id: nil, name: "Galletas Gama", description: nil, stock: 5, price: 0.1e2, created_at: nil, updated_at: nil>
product.save # false
# => El objeto product arroja false debido a que no puede guardar el registro porque el objeto no está estructurado
# según la validación que colocamos en el modelo sobre el campo descripción
MÉTODO VALID? & INVALID?
Este método proveerá una respuesta booleana que nos informará si el objeto cumple con las validaciones y puede persistirse o no.
# app/models/product.rb
validates :description, presence: true
product = Product.new name: 'Galletas Gama', description: 'Galletas', stock: 5, price: 10
product.valid? # => true
product.invalid? # => false
product = Product.new name: 'Galletas Gama', stock: 5, price: 10
product.valid? # => false
product.invalid? # => true
MÉTODO PERSISTED?
Nos permite conocer si un objeto se ha podido persistir como un registro en la base de datos o no. Este método deberá de utilizarse luego de la ejecución del comando save.
# app/models/product.rb
validates :description, presence: true
product = Product.new name: 'Galletas Gama', description: 'Galletas', stock: 5, price: 10
product.valid? # => true
product.save
product.persisted? # => true
product = Product.new name: 'Galletas Gama', stock: 5, price: 10
product.valid? # => false
product.save
product.persisted? # => false
MÉTODOS CHANGED?, CHANGED & CHANGES
Estos métodos, podrán informarnos sobre el estado de los atributos del objeto y nos permitirán conocer si el objeto cambio, qué campo lo hizo y qué valor poseía antes y el valor que posee después. A continuación, un ejemplo:
product = Product.new name: 'Galletas Gama', description: 'Galletas saladas', stock: 5, price: 10
product.save
# TRANSACTION (0.2ms) BEGIN
Product Create (0.6ms) INSERT INTO `products` (`name`, `description`, `stock`, `price`, `created_at`, `updated_at`) VALUES ('Galletas Gama', 'Galletas saladas', 5, 10.0, '2022-06-02 04:56:45.414539', '2022-06-02 04:56:45.414539')
# TRANSACTION (44.3ms) COMMIT
product.stock = 10
product.changed? # => true | Validamos si el objeto cambió
product.changed # => ["stock"] | Obtenemos el campo que se ha modificado
product.changes # => {"stock"=>[5, 10]} | Requerimos los cambios que se hicieron y cual era el valor anterior.
VALIDATES
Estas validaciones nos proveen diferentes opciones predefinidas que podemos agregar a nuestro modelo y que nos permitirán asegurarnos que los registros cumplirán con los requerimientos mínimos que buscamos que se cumplan antes de persistir un objeto como registro en la base de datos.
# app/models/product.rb
validates :name, presence: { message: 'El campo nombre no ha sido correctamente introducido' }
validates :description, presence: { message: 'El campo descripción no ha sido correctamente introducido' }
validates :price, numericality: { greater_than_or_equal_to: 1, message: 'El precio %{value} debe ser mayor o igual a 0' }
Luego, si probamos algunos escenarios, podremos darnos cuenta que las validaciones ya están funcionando tanto en nuestras vistas como en la consola de Rails.
product = Product.new name: 'Galletas Gama', stock: 5, price: 0 # => #<Product id: nil, name: "Galletas Gama", description: nil, stock: 5, price: 0.0, created_at: nil, updated_at: nil>
product.save # => false
product.persisted? # => false
Ahora, aunque sabemos que el registro no se ha persistido, necesitamos conocer cuales son las validaciones que no se cumplieron. Para ello, podemos hacer uso del método errors.
MÉTODO ERRORS
El método errors, retorna mediante un objeto Error, una lista los atributos y el mensaje de error de las validaciones que no se cumplen. Este método, posee algunos atributos que pueden imprimirse en nuestra aplicación. Por ejemplo, para el escenario anterior, podríamos obtener la siguiente información:
product = Product.new name: 'Galletas Gama', stock: 5, price: 0
product.save
product.errors # => #<ActiveModel::Errors:0x00007f79d4f727e0 @base=#<Product id: nil, name: "Galletas Gama", description: nil, stock: 5, price: 0.0, created_at: nil, updated_at: nil>, @errors=[#<ActiveModel::Error attribute=description, type=blank, options={:message=>"El campo descripción no ha sido correctamente introducido"}>, #<ActiveModel::Error attribute=price, type=greater_than_or_equal_to, options={:message=>"El precio %{value} debe ser mayor o igual a 0", :value=>0, :count=>1}>]>
product.errors.messages
# => {:description=>["El campo descripción no ha sido correctamente introducido"], :price=>["El precio 0 debe ser mayor o igual a 0"]}
product.errors.full_messages
# => => ["Description El campo descripción no ha sido correctamente introducido", "Price El precio 0 debe ser mayor o igual a 0"]
product.errors.details
# => {:description=>[{:error=>:blank}], :price=>[{:error=>:greater_than_or_equal_to, :value=>0, :count=>1}]}
CUSTOM VALIDATE
Esta validación, es comúnmente utilizada para comprobar el estado de un campo, el cual requiere de una validación más personalizada y específica para el tipo de contenido que va a alojar el mismo. Para esta validación, utilizamos un método que estará incluido entre los métodos de instancia de nuestra clase y así, podremos manejar la respuesta que necesitamos, creando un error con un objeto Error.
# app/models/product.rb
validate :description_validate
private
def description_validate
unless description.nil?
errors.add(:description, 'La descripción debe tener más de 10 caracteres') unless description.length > 10
end
end
Ahora, si probamos esta validación en consola, obtendremos el siguiente error en la lista.
product = Product.new name: 'Galletas Gama', description: 'Galletas', stock: 5, price: 1
# => #<Product id: nil, name: "Galletas Gama", description: "Galletas", stock: 5, price: 0.1e1, created_at: nil, updated_at: nil>
product.save # => false
product.errors.messages # => {:description=>["La descripción debe tener más de 10 caracteres"]}
VALIDATES_WITH
Este último método nos permite crear un error de un objeto a guardar, que pueda tener un uso más global sobre toda nuestra aplicación. Este método, deberá de estar dentro de los concerns de los modelos, tal cual lo hacen los módulos, como un código adicional de nuestros modelos.
# app/models/product.rb
validates_with ProductValidator
# app/concerns/product_validator.rb
class ProductValidator < ActiveModel::Validator
def validate(record)
validate_stock(record)
end
def validate_stock(record)
record.errors.add :stock, message: "El campo stock debe ser tener un valor mayor que 0" unless record.stock > 0
end
end
CREAR UNA MIGRACIÓN PARA AGREGAR MÁS CAMPOS
En la consola:
rails g migration add_code_and_uuid_fields_to_products code:string uuid:text
# create db/migrate/20220603063455_add_code_and_uuid_fields_to_products.rb
En el archivo de la migración, que hemos creado únicamente agregaremos el argumento after para que los campos aparezcan luego de las columnas que requerimos:
# db/migrate/20220603063455_add_code_and_uuid_fields_to_products.rb
class AddCodeAndUuidFieldsToProducts < ActiveRecord::Migration[6.1]
def change
add_column :products, :code, :string, after: :id
add_column :products, :uuid, :text, after: :name
end
end
Luego, corremos la migración para que estos campos se agreguen a nuestra tabla:
rails db:migrate
# == 20220603063455 AddCodeAndUuidFieldsToProducts: migrating ===================
# -- add_column(:products, :code, :string, {:after=>:id})
# -> 0.0505s
# -- add_column(:products, :uuid, :text, {:after=>:name})
# -> 0.0065s
# == 20220603063455 AddCodeAndUuidFieldsToProducts: migrated (0.0572s) ==========
Para realizar las siguientes pruebas, es necesario crear algunos registros utilizando la gema Faker. Esta gema se agregó en la lección 05 de este Wiki, y si la misma ya está instalada, también puede usarse en la consola y nos servirá para crear nombres de productos ficticios con descripciones random del lorem ipsum. Con el siguiente comando, crearemos 50 registros con sus respectivos campos rellenados.
50.times { Product.create name: "#{Faker::Commerce.product_name}", code: "P#{Time.now.strftime("%y%m%d")}#{Random.rand(1..999).to_s}", uuid: SecureRandom.uuid, description: Faker::Lorem.sentence, price: Random.rand(1..5000), stock: Random.rand(1..25) }