08. ORM & Active Records CRUD Methods - williamromero/curso-rails GitHub Wiki
OBTENER UN OBJETO - ActiveRecord::FinderMethods
# Obtener primer registro
Product.first
# Product Load (0.2ms) SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1
# => #<Product id: 1, name: "Pepsi Cola", description: "Bebida de soda", stock: 1, price: 0.55e1, created_at: "2022-05-30 13:59:13.599556000 +0000", updated_at: "2022-06-02 16:53:34.419952000 +0000">
# Obtener último registro
Product.last
# Product Load (0.3ms) SELECT `products`.* FROM `products` ORDER BY `products`.`id` DESC LIMIT 1
# => #<Product id: 16, name: "Galletas Gama", description: "Galletas saladas", stock: 5, price: 0.0, created_at: "2022-06-02 05:51:21.009150000 +0000", updated_at: "2022-06-02 05:51:21.009150000 +0000">
# Obtiene todos los registros
Product.all
# El método find obtiene el primer registro que posea el id, número 41.
product = Product.find 41 # => 1 record
# => #<Product id: 41, code: "P220603270", name: "Ergonomic Steel Table", uuid: "1d2772f9-c66d-4816-82b3-2a7017ade538", description: "Maiores in sed cumque.", stock: 15, price: 0.2813e4, created_at: ..., updated_at: ... >Buscando registros por llaves primarias.
# USOS DEL FIND
# Obtener una lista de productos por atributo id
product = Product.find 31, 45, 51
product.size # => 3 products on array.
# => [#<Product id: 31, code: "P220603550", name: "Awesome Iron Knife", uuid: "6fcfa595-ae8b-46d6-876d-7c3692e3daf9", description: "Sunt sed eius minima.", stock: 20, price: 0.3889e4, created_at: "2022-06-03 06:56:29.742630000 +0000", updated_at: "2022-06-03 06:56:29.742630000 +0000">, #<Product id: 45, code: "P220603957", name: "Gorgeous Paper Plate", uuid: "1b0ffb97-8a0c-481b-98a6-e9ed146f1723", description: "Aut officiis quo magni.", stock: 23, price: 0.1467e4, created_at: "2022-06-03 06:56:29.816677000 +0000", updated_at: "2022-06-03 06:56:29.816677000 +0000">, #<Product id: 51, code: "P220603522", name: "Lightweight Cotton Pants", uuid: "ecb9fe3d-9cd9-4eb8-a373-468ba4a2eba9", description: "Et repellendus unde autem.", stock: 9, price: 0.1826e4, created_at: "2022-06-03 07:05:22.350961000 +0000", updated_at: "2022-06-03 07:05:22.350961000 +0000">]
# get error when id not exists
product = Product.find 31, 43, 58 #=> throws Exception: `raise_record_not_found_exception!': Couldn't find all Products with 'id': (31, 43, 58) (found 2 results, but was looking for 3). (ActiveRecord::RecordNotFound)
# find product by attribute
product = Product.find_by code: 'P43211' # => 1 record
# => 1 record <Product:0x00007fadb95bfd60 id: 1, title: "Producto 1", code: "P001", stock: 10, price: 100000, uuid: "a0eb2f7f-55d9-4c00-a761-d43af856697e" > # En consola
Product.where(price: 100000..400000) # Obtiene todos los productos que tengan un precio entre 100000 y 400000
# Obtiene todos los productos los cuales su stock sea igual a 20.
Product.where(stock: 20)
# => [#<Product:0x00007fc28087e9d0 id: 2, title: "Producto 2", stock: 20, price: 200000, created_at: Fri, 08 Apr 2022 02:19:24.250355000 UTC +00:00, updated_at: Fri, 08 Apr 2022 02:19:24.250355000 UTC +00:00>]
# Obtiene los productos que su precio esté entre 100000 y 400000 y que su stock sea menor que 20
Product.where(price: 100000..400000).where('stock < ?', 20)
# Obtiene los registros que estén entre el rango de precio, que tengan un stock de 20
# pero lo más importante, nos devuelve el número de registros.
Product.where(price: 100000..400000, stock: 20).count # => 1 record
# Obtiene los registros con un precio menor o igual a 200000 y que posean un stock menor a 30
Product.where('price <= ? and stock < ?', 200000, 30) # => 2 records
# El método OR nos ayuda a concatenar nuevas relaciones usando únicamente el método WHERE.
Product.where(price: 200000..400000).or(Product.where('title LIKE ?', "%Prod")).or(Product.where('stock < ?', 40))
# => 4 recordsEsta opción, se utiliza normalmente cuando se debe de iterar sobre un gran monto de registros. Por ejemplo, a la hora de enviar un correo a una base de usuarios, será más fácil manipular mil registros por cada X segundos, que una tabla completa de millones de registros que luego deban de realizar una acción. A continuación un ejemplo:
User.each { |user| NewsLetter.weekly_deliver(user) }Para esto, se utilizan diferentes métodos pero quizás el más común es find_in_batches. A continuación, tomaré un ejemplo de nuestra base de datos la cual hemos crecido hasta 200 registros.
Product.select([:id, :name, :price]).find_in_batches(batch_size: 100, start:0).each do |prd|
p prd.select {|p| p.price < 500 }.size #
end
# Product Load (0.4ms) SELECT `products`.`id`, `products`.`name`, `products`.`price` FROM `products` WHERE `products`.`id` >= 0 ORDER BY `products`.`id` ASC LIMIT 100
# => 12
# Product Load (0.6ms) SELECT `products`.`id`, `products`.`name`, `products`.`price` FROM `products` WHERE `products`.`id` >= 0 AND `products`.`id` > 100 ORDER BY `products`.`id` ASC LIMIT 100
# => 6Si nos fijamos en las dos peticiones que hará a la base de datos, estas irán de 0 a 100 y luego de 100 hacia dela y así cada 100 registros. La guía de referencia siempre será el ID, por lo que si hay registros eliminados previamente, los resultados pueden variar. A continuación, otro ejemplo de uso del find_in_batches:
Product.find_in_batches(batch_size: 100, start:0).each { |prd| p prd.select {|p| p.stock < 2 }.size }
# Product Load (0.5ms) SELECT `products`.* FROM `products` WHERE `products`.`id` >= 0 ORDER BY `products`.`id` ASC LIMIT 100
# => 3
# Product Load (0.6ms) SELECT `products`.* FROM `products` WHERE `products`.`id` >= 0 AND `products`.`id` > 100 ORDER BY `products`.`id` ASC LIMIT 100
# => 2
# Otro ejemplo:
Product.find_in_batches(batch_size: 100, start: 0).each { |prd| p prd.select { |record| record.price < 200 }.size }
# SQL Query => 5
# SQL Query => 9
# SQL Query => 3
# Result of records => 17
filtered_products = []
Product.find_in_batches(batch_size: 100, start: 0).each { |prd| prd.select { |record| filtered_products << record unless record.price > 200 } }
filtered_products.size # => 17Una tarea que es necesaria dentro de los requerimientos que comúnmente se hacen a la base de datos es la de contabilizar en número de registros que contiene una consulta. En ROR, existen tres métodos para realizar dicha tarea que veremos a continuación. Estos funcionan de diferentes formas, pero responden a un funcionamiento común:
TODAS LAS PETICIONES EN RUBY ON RAILS, SON CARGADAS EN MEMORIA QUE SE EJECUTAN HASTA QUE LA INFORMACIÓN VA A UTILIZARSE EN EL CÓDIGO, POR LO QUE EL EMPLEO DEL MÉTODO LOAD ES NECESARIO PARA OBTENER LOS REGISTROS DE LA BASE DE DATOS SI NO SE HAN SOLICITADO ANTES PARA OTRA TAREA. ES DECIR,
METODO COUNT
Cuando el método es llamado en un objeto ActiveRecord Relation, el método count ejecutará una consulta SQL COUNT.
products = Product.count
# (0.6ms) SELECT COUNT(*) FROM `products`
# => 500METODO LENGTH
length es un método plano de Ruby que sirve para contar cómo el número de elementos, como por ejemplo, de un array. La diferencia es que al no existir un método length* nativo del ActiveRecord, cuando hacemos el llamado del método length con un objeto *ActiveRecord Relation, este lo convierte en un array y cuenta el número de objetos. Esta tarea, es obviamente más lenta que el count.
Un aspecto importante a tomar en cuenta es que si la lista de objetos aún no está loaded?, el método ejecutará la operación load para poder obtener los registros y contarlos.
products = Product.all
# Product Load (0.4ms) SELECT `products`.* FROM `products` /* loading for inspect */ LIMIT 11
# => #<ActiveRecord::Relation [#<Product id: 307, code: "P220608958", name: "Gorgeous Silk Shoes", uuid: "9f7117b1-21e0-4fce-91de-db526b2da2c3", description: "Et ...
products.length # => 500
# Product Load (1.0ms) SELECT `products`.* FROM `products`
products.length
# => 500MÉTODO SIZE
Como el método count, size funciona diferente dependiendo de si lo llama en un array o en un objeto ActiveRecord Relation. Cuando es llamado por un array, size funciona igual que el método length: retorna el número de objetos en el array. Pero las cosas se vuelven interesantes cuando se llama al método desde una ActiveRecord::Relation. En ese caso, su comportamiento es diferente dependiendo de si la relación de registros ha sido cargada de la base de datos o no. Si los registros ya han sido cargados, entonces podremos llamar el método size para obtener el tamaño de la asociación (ActiveRecord::Relation), sino, este hará una query a la base de datos para obtener el valor.
products = Product.all
# Product Load (0.3ms) SELECT `products`.* FROM `products` /* loading for inspect */ LIMIT 11
products.load
# Product Load (0.9ms) SELECT `products`.* FROM `products`
# => <ActiveRecord::Relation [#<Product id: 307, code: "P220608958", name: "Gorgeous Silk Shoes", uuid: "9f7117b1-21e0....
products.size
# => 500
# SI EL PRODUCTO NO ESTÁ CARGADO
products = Product.all
# Product Load (0.3ms) SELECT `products`.* FROM `products` /* loading for inspect */ LIMIT 11
# El metodo SIZE ejecutará la query a la base de datos con el método count
# (0.8ms) SELECT COUNT(*) FROM `products`
# => 500SELECT
El método SELECT nos permite elegir las columnas que necesitamos evitando obtener todos los campos de un grupo de registros.
products = Product.order(price: :desc).limit(3).select :id, :title, :price
# Product Load (0.5ms) SELECT `products`.`id`, `products`.`title`, `products`.`price` FROM `products` ORDER BY `products`.`price` DESC LIMIT 3
# [<Product:0x00007fe325e8aac0 id: 43, title: "Lightweight Paper Table", price: 9953368>,
# <Product:0x00007fe325e8a9f8 id: 8, title: "Synergistic Leather Shoes", price: 9944133>,
# <Product:0x00007fe325e8a930 id: 48, title: "Practical Rubber Bench", price: 9914656> ]El método select, siempre regresa una lista de objetos.
PLUCK
El método PLUCK funciona de forma similar que el método SELECT pero la diferencia principal entre ambos métodos es la forma en que retorna la información que es requerida. El método pluck retorna una lista de valores depositados entre corchetes []. Cuando se requiere más de una columna, esto retorna una lista de listas.
products = Product.order(price: :desc).limit(3).pluck :id, :title, :price
# Product Pluck (0.4ms) SELECT `products`.`id`, `products`.`title`, `products`.`price` FROM `products` ORDER BY `products`.`price` DESC LIMIT 3
# => [[43, "Lightweight Paper Table", 9953368], [8, "Synergistic Leather Shoes", 9944133], [48, "Practical Rubber Bench", 9914656]]FIND_OR_CREATE_BY
El método find_or_create_by busca el primer registro con los atributos dados o crea un registro con los atributos dados si no lo ha encontrado:
product = Product.find_or_create_by title: 'Green Tshirt', stock: 40, price: '900000', code: Random.rand(40000..90000)
# FINDING PRODUCT
# Product Load (0.3ms) SELECT `products`.* FROM `products` WHERE `products`.`title` = 'Green Tshirt' AND `products`.`stock` = 40 AND `products`.`price` = 900000 AND `products`.`code` = '72486' LIMIT 1
# TRANSACTION (0.1ms) BEGIN
# Product Exists? (0.2ms) SELECT 1 AS one FROM `products` WHERE `products`.`code` = '72486' LIMIT 1
# IF PRODUCT DOESN'T EXISTS, ACTIVE RECORD WILL CREATE IT
# "Un nuevo producto será añadido a almacen"
# Product Create (0.3ms) INSERT INTO `products` (`title`, `code`, `stock`, `price`, `uuid`, `created_at`, `updated_at`) VALUES ('Green Tshirt', '72486', 40, 900000, '679f3776-1704-4a12-bb80-2fe88c79a151', '2022-04-22 04:58:50.536904', '2022-04-22 04:58:50.536904')
# "Un nuevo producto fue añadido a almacén Green Tshirt - 9000.0 USD"
# TRANSACTION (52.4ms) COMMIT
# Product id: 56, title: "Green Tshirt", code: "72486", stock: 40, price: 900000, uuid: "679f3776-1704-4a12-bb80-2fe88c79a151", created_at: "2022-04-22 04:58:50.536904000 +0000", updated_at: "2022-04-22 04:58:50.536904000 +0000">Los scopes son peticiones predefinidas las cuales se especifican dentro del modelo. Este método crea un método de clase, por lo que no es necesario instanciar un objeto para hacer nuestra petición a la base de datos. Una consideración importante de los scopes es que deben de respetar el PRINCIPIO DE RESPONSABILIDAD ÚNICA, porque los mismos podrán ser concatenados entre ellos para responder la información que requiere el cliente.
# Scopes
scope :availables, -> { where('stock > 0') }
scope :cheaper_products, -> ( precio ) { where('price < ?', precio) }
scope :ordered_by_price, -> { order(price: :desc) }Después, puedes testearlo en la consola:
products = Product.available # SELECT `products`.* FROM `products` WHERE (stock >= '1')
products.size # => 57
products = Product.available_maximium_products 10 # SELECT `products`.* FROM `products` WHERE (stock <= '10')
products.size # => 24
# Concatenated request using scopes
products = Product.available_maximium_products(10).order_price_desc
# or
products = Product.available_and_order_price_desc # Product Load (0.4ms) SELECT `products`.* FROM `products` WHERE (stock >= '1') ORDER BY `products`.`price` DESC
products.size # 57Los scopes no deben de ser utilizados para realizar comparaciones a través de registros, pero con los métodos de clase, podemos solicitar, construir y establecer consultas importantes y complejas que requieran interacción con los registros que estemos obteniendo.
# app/models/product.rb
def self.top_five_available
self.available.order_price_desc.limit(5).select(:title, :price)
endY en la consola:
products = Product.top_five_available # Product Load (0.4ms) SELECT `products`.`title`, `products`.`price` FROM `products` WHERE (stock >= '1') ORDER BY `products`.`price` DESC LIMIT 5
products.size # 5 records
# <Product:0x00007f93cff03880 id: nil, title: "Lightweight Paper Table", price: 9953368>,
# <Product:0x00007f93cfef4718 id: nil, title: "Synergistic Leather Shoes", price: 9944133>,
# <Product:0x00007f93cfef4650 id: nil, title: "Practical Rubber Bench", price: 9914656>,
# <Product:0x00007f93cfef4588 id: nil, title: "Durable Leather Watch", price: 9733173>,
# <Product:0x00007f93cfef44c0 id: nil, title: "Durable Linen Shirt", price: 9374968>Para actualizar los registros en Rails, tenemos diferentes métodos para hacerlo dependiendo de los escenarios.
product = Product.find 14
product.name = 'Flashing Lights'
product.save
# TRANSACTION (0.2ms) BEGIN
Product Update (1.9ms) UPDATE `products` SET `products`.`title` = 'Flashing Lights', `products`.`uuid` = 'a1d1edf5-09a8-43b5-ba02-d3a8efbd6d81', `products`.`updated_at` = '2022-05-30 01:21:27.244663' WHERE `products`.`id` = 14
# TRANSACTION (2.0ms) COMMIT (update)
Para utilizar el método update, tenemos que tener un objeto instanciado para que podamos actualizar sus campos.
product = Product.first
product.update title: 'iMac Pro', stock: 20, price: 500000
# TRANSACTION (0.2ms) BEGIN
Product Update (0.4ms) UPDATE `products` SET `products`.`title` = 'iMac Pro', `products`.`code` = 'P45099', `products`.`stock` = 20, `products`.`price` = 500000, `products`.`uuid` = '9fa8233c-62f5-4e08-9a78-f63e2722451d', `products`.`updated_at` = '2022-05-30 01:55:00.937094' WHERE `products`.`id` = 1
# TRANSACTION (2.0ms) COMMIT Este método únicamente correrá únicamente si pasa todas las validaciones del modelo. Si existe alguna validación que haya tenido un problema y por ello, obtengamos una excepción, obtendremos un rollback, que significa que el registro no pudo ser guardado.
product.update stock: 0
# TRANSACTION (0.1ms) BEGIN
# Product Exists? (0.4ms) SELECT 1 AS one FROM `products` WHERE `products`.`code` = 'P45099' AND `products`.`id` != 1 LIMIT 1
TRANSACTION (0.2ms) ROLLBACK
product.errors.messages
# {:stock=>["El stock debe ser mayor a 0"]}(update_attribute)
A través de este método, nosotros seremos capaces de modificar solo ÚN campo de nuestro objeto instanciado. Este método evita las validaciones, por lo que es necesario usar este comando cuidadosamente.
product = Product.last
product.update_attribute :title, "Tshirt Real Madrid Season"
# UPDATE `products` SET `products`.`title` = 'Tshirt Real Madrid Season 22-23', `products`.`uuid` = '2d563cea-1d09-4f30-bf80-9dc7a79ddd91', `products`.`updated_at` = '2022-05-30 02:35:10.471173' WHERE `products`.`id` = 57
product.update_attribute :stock, 0
# UPDATE `products` SET `products`.`stock` = 0, `products`.`uuid` = '0473fdda-3e6f-4c74-8f09-0e80a8c2ec0e', `products`.`updated_at` = '2022-05-30 02:36:28.366937' WHERE `products`.`id` = 57(update_attributes)
Método en desuso por método de update
A diferencia del comando update_attribute, el comando update_attributes solo se ejecuta si todas las validaciones se realizan correctamente.
product = Product.second
product.update_attributes title: 'iPad Pro', stock: 0, code: "P55204"(update!)
Este método arroja una excepción cuando la modificación del registro es inválida.
product.update! stock: 0
# /Users/williamromero/.rvm/gems/ruby-3.0.0/gems/activerecord-7.0.2.2/lib/active_record/validations.rb:80:in `raise_validation_error': Validation failed: Stock El stock debe ser mayor a 0 (ActiveRecord::RecordInvalid)(update_column)
Este método nos permite la modificación de una columna para nuestra tabla y las validaciones y callbacks serán omitidas. También, la columna updated_at no será actualizado.
product = Product.last
# Product Load (0.4ms) SELECT `products`.* FROM `products` ORDER BY `products`.`id` DESC LIMIT 1
product.updated_at
# Wed, 08 Jun 2022 17:10:33.625950000 UTC +00:00
product.update_column :stock, 10
# Product Update (45.8ms) UPDATE `products` SET `products`.`stock` = 10 WHERE `products`.`id` = 800
product.updated_at
# Wed, 08 Jun 2022 17:10:33.625950000 UTC +00:00 Este método solo debe de ser usado si el performance de respuesta es algo crítico debido a que se gana velocidad pero se pierde certeza o seguridad.
(update_columns)
Mismo que le método update_column pero se pueden actualizar múltiples atributos.
product = Product.last
product.update_columns stock: 10, code: 'P00241'
Product Update (12.3ms) UPDATE `products` SET `products`.`stock` = 10, `products`.`code` = 'P00241' WHERE `products`.`id` = 57
# => trueEl siguiente código es usado para remover productos individualmente.
product = Product.last
product.destroy # This destroy the objectEl siguiente código, va a ser utilizado para remover múltiples productos.
Product.where("stock <= ?", 2).destroy_all
# Product Load (0.5ms) SELECT `products`.* FROM `products` WHERE (stock <= '2')
# TRANSACTION (0.2ms) BEGIN
Product Destroy (1.5ms) DELETE FROM `products` WHERE `products`.`id` = 26
# TRANSACTION (2.0ms) COMMIT
# TRANSACTION (0.3ms) BEGIN
Product Destroy (0.6ms) DELETE FROM `products` WHERE `products`.`id` = 37
# TRANSACTION (1.0ms) COMMIT
# => [#<Product>,<Product>]
# This destroy the records product = Product.first
# Revisar si alguno de los atributos que han cambiado
product.changed? # => false
product.stock = 200 # 200
product.changed? # => true
product.changes # => {"stock"=> [20, 200]}
product.stock_changed? # => true
product.title_changed? # => false
# Revisa el valor previo cuando el atributo ha sido cambiado
product.stock_was # => 20
product.save
# Product Update (0.9ms) UPDATE `products` SET `products`.`stock` = 200, `products`.`uuid` = 'b60caeb2-4240-477d-b42e-fd680745f721', `products`.`updated_at` = '2022-05-30 05:54:56.501482' WHERE `products`.`id` = 1
product.changed? # => {} El estado retorna el estado inicial y cambios si el mismo no se ha persistido. class Product < ApplicationRecord
# Validations
before_update :code_notification_changed, if: :code_changed?
# Following code
def code_changed?
puts "\n\n\n >>>>> El código ha sido modificado <<<<< \n\n\n"
end
endAhora si el atributo código es cambiado:
product = Product.last
product.code = "P20939"
product.save
# TRANSACTION (0.2ms) BEGIN
# Product Exists? (0.4ms) SELECT 1 AS one FROM `products` WHERE `products`.`code` = 'P20939' AND `products`.`id` != 57 LIMIT 1
# "Un nuevo producto será añadido a almacen"
El código ha sido modificado
#Product Update (0.6ms) UPDATE `products` SET `products`.`code` = 'P20939', `products`.`uuid` = '1698fba4-6918-4ae7-9634-9c89fe04f00d', `products`.`updated_at` = '2022-05-30 05:58:58.511540' WHERE `products`.`id` = 57
# "Un nuevo producto fue añadido a almacén Tshirt Real Madrid Season 22-23 - 5000.0 USD"
# TRANSACTION (2.0ms) COMMIT Cuando el registro es persistido
product = Product.first
product.stock = 400
product.save
product.saved_change_to_stock? # => true
product.saved_change_to_title? # => falseAquí un particular ejemplo de callbacks:
class Product < ApplicationRecord
# Validations
after_update :send_notification_stock, if: :stock_limit?
# Following code
private
def stock_limit?
self.saved_change_to_stock? && stock <= 5
end
endAhora, si nosotros editamos el stock de nuestro producto:
product = Product.first
product.stock = 2
product.save
#TRANSACTION (0.3ms) BEGIN
#Product Exists? (0.4ms) SELECT 1 AS one FROM `products` WHERE `products`.`code` = 'P45099' AND `products`.`id` != 1 LIMIT 1
# "Un nuevo producto será añadido a almacen"
# >>>>> El código ha sido modificado <<<<<
# Product Update (0.4ms) UPDATE `products` SET `products`.`stock` = 2, `products`.`uuid` = '7578fe4f-f10a-430f-b841-fcd1f93ba8b1', `products`.`updated_at` = '2022-05-30 06:16:19.572856' WHERE `products`.`id` = 1
>>>>> El producto iMac Pro se encuentra escaso en almacen: 2 <<<<<
# "Un nuevo producto fue añadido a almacén iMac Pro - 5000.0 USD"
# TRANSACTION (1.4ms) COMMIT
# => trueLoading for Inspect message when records are not loaded
How to count with active record
ActiveRecord::Association Object Count`
ActiveRecord database performance improvement
Differences between ActiveRecord & ActiveRecord::Relation object
Difference of ActiveRecord::Relation object
Get just the first limited record of a Relation with PICK method