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) }

References

ActiveModel Basics