Maior performance no seu sistema
com o uso adequado de
ORM em Rails
Vamos usar outra linguagem
Ruby é lento
Vamos fragmentar o sistema
Precisamos reescrever do zero
Será que o problema é a
linguagem ou o framework?
Ou não estamos usando
eles de forma adequada?
ORM
EM
RAILS
Object
Relational
Mapping
Rails nos oferece
Active
Record
Ele é um Pattern!
Martin Fowler
Order
Customer
Order Items
Product
Shipping Company
Delivery
>Order.create(customer:...)
>Order.find(1).customer.name
>Order.find_by(customer:1).order_items.count
>Order.all
class Customer < ApplicationRecord
end
class Delivery < ApplicationRecord
belongs_to :shipping_company
belongs_to :ordem_item
end
class Order < ApplicationRecord
belongs_to :customer
has_many :order_items
end
class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :product
has_one :delivery
end
class Product < ApplicationRecord
end
class ShippingCompany < ApplicationRecord
end
Order Load (1.0ms) SELECT "orders".* FROM
"orders" LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Order id: 1,
customer_id: 2, status: "ENTREGUE", ...]>
<table>
<thead>
<tr>
<th>Id</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% @orders.each do |order| %>
<tr>
<td><%= order.id %></td>
<td><%= order.status %></td>
</tr>
<% end %>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Customer</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% @orders.each do |order| %>
<tr>
<td><%= order.customer.name %></td>
<td><%= order.status %></td>
</tr>
<% end %>
</tbody>
</table>
Para 10.000 orders, até 10.001 consultas ao banco
(Queries N+1)
def index
@orders = Order.all
end
def index
@orders = Order.all.includes(:customer)
end
2 consultas ao banco de dados
def index
@orders = Order.all.joins(:customer).includes(:customer)
end
1 consulta ao banco de dados
Default
16171ms
9408ms
9574ms
9783ms
17361ms
19387ms
13.6s
Qual melhor estratégia?
Include
1490ms
1440ms
1844ms
1581ms
1555ms
1969ms
1.6s
Join + Include
2044ms
2168ms
1536ms
1903ms
1978ms
1668ms
1.8s
+ db index
1579ms
1653ms
1575ms
1556ms
1613ms
1564ms
1.5s
Como coletar
essas métricas?
Uso do log do rails server
Application
Performance
Management (APM)
15ms
1ms
https://github.com/newrelic/rpm
Porque esses
problemas ocorrem ?
SOLUÇÃO CRESCE !
● FEATURES COMPLEXAS
O TIME CRESCE !!!
● MAPEAMENTOS EQUIVOCADOS
● MAU USO DE NAVEGAÇÃO
ENTRE OBJETOS
● MODELAGEM CONFUSA
Como evitar
esses problemas ?
Bullet
https://github.com/flyerhzm/bullet
DB Query Matchers
https://github.com/brigade/db-query-matchers
teste A
teste B
teste C
teste D
teste E
teste F
teste G
teste H
teste I
teste J
teste K
teste L
require 'rails_helper'
feature 'orders index' do
scenario 'list all' do
expect { visit('/orders') }.to make_database_queries(count: 3)
end
scenario 'list all' do
expect { visit('/orders_include') }.to make_database_queries(count: 2)
end
scenario 'list all' do
expect { visit('/orders_join') }.to make_database_queries(count: 1)
end
end
expect { }.to_not make_database_queries(matching: /UPDATE/)
expect { }.to_not make_database_queries
expect { }.to make_database_queries(count: 1)
expect { }.to make_database_queries(count: 0..3)
expect { }.to_not make_database_queries(manipulative: true)
(INSERT, UPDATE, DELETE)
Boas
Práticas
Uso de Scope
def index_with_join
@orders = Order.all.joins(:customer).includes(:customer)
end
Controller
class Order < ApplicationRecord
belongs_to :customer
has_many :order_items
scope :all_with_customer, -> {Order.all.joins(:customer).includes(:customer)}
end
Model
def index_with_join
@orders = Order.all_with_customer
end
Controller
Evite...
scope :all_with_customer, -> {
Order.all.includes(:customer).order(:status)}
Ordenação
default_scope :active, -> {Customer.where(active:true)}Default
scope :closed, -> {Order.where(status:‘closed’)
.where(‘total_amount >= ?’, 10.0)}
Itens
Implícitos
scope :closed, -> do
find_by_sql <<-SQL
select * from orders where status = ‘closed’
SQL
end
SQL puro
Queries customizadas com
Mapeamento Alternativos
class SummaryOrder < ApplicationRecord
self.table_name = "orders"
scope :all_summarized, -> {
select('orders.id')
.select('customers.name')
.select('orders.status')
.select('count(order_items.id) as items_quantity')
.joins('left join customers on orders.customer_id = customers.id')
.joins('right join order_items on order_items.order_id = orders.id')
.group('orders.id, customers.name, orders.status')
}
end
...
<tr>
<th>Customer</th>
<th>Status</th>
<th>Quantity of Items</th>
</tr>
...
<% @summary_orders.each do |summary_order| %>
<tr>
<td><%= summary_order.name %></td>
<td><%= summary_order.status %></td>
<td><%= summary_order.items_quantity %></td>
</tr>
<% end %>
...
class SummaryOrder
attr_accessor :id,
:name,
:status,
:items_quantity
end
count, length ou size ?
count
length
size
OO ou SQL?
Dê preferência ao uso de OO!
OO não pode sacrificar performance
ORM é um facilitador, conhecer SQL e banco de dados
ainda é importante
Não abra mão do poder do banco de dados
> SummaryOrder.all_summarized.explain
HashAggregate (cost=871.09..971.09 rows=10000 width=57)
Group Key: orders.id, customers.name
-> Hash Left Join (cost=349.70..796.09 rows=10000 width=57)
Hash Cond: (orders.customer_id = customers.id)
-> Hash Left Join (cost=319.00..639.24 rows=10000 width=33)
Hash Cond: (order_items.order_id = orders.id)
-> Seq Scan on order_items (cost=0.00..194.00 rows=10000 width=16)
-> Hash (cost=194.00..194.00 rows=10000 width=25)
-> Seq Scan on orders (cost=0.00..194.00 rows=10000 width=25)
-> Hash (cost=19.20..19.20 rows=920 width=40)
-> Seq Scan on customers (cost=0.00..19.20 rows=920 width=40)
Explain
Vimos resumidamente...
Includes
Joins Scopes
Mapeamentos Alternativos
New Relic Bullet
DB Query Matchers
Queries N+1
Count, Length, Size
Explain
Isso resolve todos
os problemas ?
provavelmente
não!
Talvez ainda seja
necessário...
● Reescrever
● Fragmentar
● Ou até usar outra
linguagem
Faça pelos
motivos adequados!
Verifique se o
problema de performance
não é mal uso de ORM
Obrigado !
Isaac Felisberto de Souza
isaacsouza@gmail.com
linkedin.com/in/isaacfsouza
Dúvidas ?

Maior performance no seu sistema com o uso adequado de orm em rails