Ruby on Rails

Parte 2

Agenda

  • Migraciones
  • Validaciones y callbacks
  • Asociaciones
  • Queries

Migraciones

Migraciones (docs)

Crear una nueva tabla:

$ rails g model Post title:string content:text
    create    db/migrate/20190724032006_create_posts.rb
    create    app/models/post.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.timestamps
    end
  end
end

Migraciones (up/down)

class CreateLinks < ActiveRecord::Migration[5.2]
  def up
    create_table :user_links do |t|
      ...
    end
    add_column :users, :home_page_url, :string
    execute <<-SQL
      ALTER TABLE products (...)
    SQL
  end

  def down
    execute <<-SQL
      ALTER TABLE products (...)
    SQL
    remove_column :users, :home_page_url
    drop_table :user_links
  end
end

Migraciones

$ rails db:migrate
== 20190724032006 CreatePosts: migrating ======================================
-- create_table(:posts) -> 0.0011s

Corre todas las migraciones pendientes

$ rails db:migrate VERSION=20190724032006
Corre las migraciones (hacia arriba o abajo) hasta llegar a la VERSION

$ rails db:rollback STEP=3
Revierte las últimas 3 migraciones

$ rails db:migrate:redo STEP=3
Revierte las últimas 3 migraciones y las vuelve a ejecutar

Migraciones

$ rails generate migration AddPartNumberToProducts part_number:string:index
class AddPartNumberToProducts < ActiveRecord::Migration[5.2]
  def change
    add_column :products, :part_number, :string
    add_index :products, :part_number

    # Otras opciones:
    #   remove_column :products, :part_number, :string
    #   change_column :products, :part_number, :text
    #   add_reference :products, :user, foreign_key: true
    #   create_join_table :products, :categories     (many to many)
  end
end

Migraciones: Seed

# db/seeds.rb
Admin.create(username: 'admin', password: 'admin')

require "faker" # https://github.com/stympy/faker
10.times do
  Book.create({
    title: Faker::Book.title,
    author: Faker::Book.author 
  })
end
$ rails db:seeds

Migraciones: otros comandos

$ rails -T db

rails db:create              # Creates the database from config/database.yml [RAILS_ENV]
rails db:drop                # Drops the database from config/database.yml [RAILS_ENV]
rails db:migrate             # Migrate the database [RAILS_ENV, VERSION]
rails db:migrate:status      # Display status of migrations
rails db:rollback            # Rolls the schema back to the previous version [STEP=n]
rails db:schema:dump         # Creates a db/schema.rb file
rails db:schema:load         # Loads a schema.rb file into the database
rails db:seed                # Loads the seed data from db/seeds.rb
rails db:setup               # Creates the database, loads the schema, and seeds the data
rails db:reset               # Idem db:setup, but drop the database first
rails db:version             # Retrieves the current schema version number

Validaciones

Validaciones (docs)

class User < ApplicationRecord
  validates :name, presence: true
  validates :username, format: { message: "only letters", with: /[a-z]{4,9}/ }
  validate :active_customer, on: :create

  def active_customer
    errors.add(:customer_id, "is not active") unless customer.active?
  end

end

Validaciones

@user = User.create(params)


<% if @user.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
 
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

Tipos de validaciones

  • presence
  • uniqueness
  • acceptance
  • inclusion (in: [...])
  • exclusion (in: [...])
  • format (with: /regex/)
  • length (minimum/maximum/...)
  • numericality (...)
  • validates_associated

Tipos de validaciones

Métodos que realizan validaciones:

  • create / create!
  • save / save!
  • update / update!

Métodos que NO realizan validaciones:

  • increment! / decrement!
  • update_all
  • update_column / update_columns
  • save(validate: false)

Validaciones personalizadas

class Invoice < ApplicationRecord
  validate :active_customer, on: :create
 
  def active_customer
    errors.add(:customer_id, "is not active") unless customer.active?
  end
end

Callbacks

Callbacks (docs)

class User < ApplicationRecord
  validates :username, :email, presence: true
  after_create :reward_referrer
 
  before_create do
    self.name = username.capitalize if name.blank?
  end

  def reward_referrer
    referrer_user.add_credits if referrer_user
  end
end

Callbacks

  1. before_validation
  2. after_validation
  3. before_save
  4. around_save
  5. before_create / before_update
  6. around_create / around_update
  7. after_create / after_update
  8. after_save
  9. after_commit/after_rollback

Asociaciones

Asociaciones (docs)

class Author < ApplicationRecord; end 
class Book < ApplicationRecord; end

Creamos un libro de un determinado autor...

@book = Book.create(published_at: Time.now, author_id: @author.id)

Borramos un autor, con todos sus libros...

@books = Book.where(author_id: @author.id)
@books.each do |book|
  book.destroy
end
@author.destroy

Con Rails

class Author < ApplicationRecord
  has_many :books, dependent: :destroy
end
 
class Book < ApplicationRecord
  belongs_to :author
end

# Crear un libro ...
@book = @author.books.create(published_at: Time.now)

# Eliminar un autor...
@author.destroy

Tipos de Asociaciones

  1. belongs_to
  2. has_one
  3. has_many
  4. has_many :through
  5. has_one :through
  6. has_and_belongs_to_many

belongs_to

has_one

has_many

has_many :through

has_one :through

has_and_belongs_to_many

¿Cuándo usar?

  • belongs_to vs has_one

  • has_many :through vs has_and_belongs_to_many

Relaciones Polimórficas

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

Métodos para belongs_to/has_one

association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association

Ej: author.create_book(params)

Opciones para belongs_to

:class_name
:counter_cache
:dependent
:foreign_key
:primary_key
:polymorphic
:optional

Opciones para has_one

Además de los que tiene belongs_to, se suman:

:as
:source
:source_type
:through

Métodos generados has_many

  • collection<<(object, ...)
  • collection.delete(object, ...)
  • collection.destroy(object, ...)
  • collection_singular_ids
  • collection.clear
  • collection.empty?
  • collection.size
  • collection.find(...)
  • collection.where(...)
  • collection.exists?(...)
  • collection.build(attributes = {}, ...)
  • collection.create(attributes = {})
  • collection.create!(attributes = {})
  • collection.reload

Single Table Inheritance (STI)

$ rails generate model user type:string name:string
class User; end

class Writer < User
end

class Editor < User
end

Single Table Inheritance (STI)

Writer.create(name: 'Tom')

# => INSERT INTO "users" ("type", "name") VALUES ('Writer', 'Tom')

Writer.all

SELECT "users".* FROM "users" WHERE "users"."type" IN ('Writer')

Queries

Queries (docs)

# Devuelven un único objeto
User.find(1)
User.first
User.last
User.find_by name: 'David'
# Devuelven una colección
User.all
User.find_each

Filtros y condiciones

Client.where(...)

Client.where("orders_count = 0")
Client.where("orders_count = ?", params[:orders])
Client.where(orders_count: params[:orders])
# SELECT * FROM clients WHERE (orders_count == 4)

Client.where(id: [1,2,3])
# SELECT * FROM clients WHERE (clients.id IN (1,2,3))

Client.where("username = 'admin'")

Filtros y condiciones

Client.order(:created_at)
Client.order("created_at")
Client.order(created_at: :desc)

Filtros y condiciones

Client.select(:name).distinct
# SELECT DISTINCT name FROM clients

Filtros y condiciones

Client.limit(5)
# SELECT * FROM clients LIMIT 5

Joins

Author.joins("INNER JOIN posts
  ON posts.author_id = authors.id
  AND posts.published = 't'")

# SELECT authors.* FROM authors
# INNER JOIN posts
# ON posts.author_id = authors.id
# AND posts.published = 't'

Joins

Category.joins(:articles)

# SELECT categories.* FROM categories
#  INNER JOIN articles ON articles.category_id = categories.id

Eager loading (Problema N+1)

clients = Client.limit(10)
 
clients.each do |client|
  puts client.address.postcode
end

Eager loading (Solución)

clients = Client.includes(:address).limit(10)
 
clients.each do |client|
  puts client.address.postcode
end

Scopes

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end
class Article < ApplicationRecord
  def self.published
    where(published: true)
  end
end

Article.published
category.articles.published

Dynamic Finders

Client.find_by_first_name(first_name)

Finding by SQL

Client.find_by_sql("SELECT * FROM clients
  INNER JOIN orders ON clients.id = orders.client_id
  ORDER BY clients.created_at desc")

Calculations

Client.count
Client.average("orders_count")
Client.sum("orders_count")
Client.minimum("age")