Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Boas Práticas em ActiveRecord

Boas Práticas em ActiveRecord

ActiveRecord é ótimo! A melhor parte do Ruby on Rails!
Porém, sem cuidado, pode se tornar o maior gargalo da sua aplicação.
Consultas mal formadas ou desnecessárias representam um grande problema de performance e/ou consumo excessivo de memória. Principalmente com grandes volumes de dados.
Essa apresentação mostra algumas boas práticas para evitar os gargalos que podem ser gerados ao acessar o banco de dados em aplicações Ruby on Rails com o ActiveRecord.

Nielson Rolim

April 26, 2017
Tweet

Other Decks in Programming

Transcript

  1. O que é ActiveRecord? ActiveRecord é o “M” - model

    - do MVC (RailsGuides) Responável por: - Lógica da aplicação - Consulta e persistência dos dados - Validação - Associações entre models
  2. Qual o problema? ActiveRecord é ótimo! A melhor parte do

    Ruby on Rails! Porém, sem cuidado, pode se tornar o maior gargalo da sua aplicação. Consultas mal formadas ou desnecessárias representam um grande problema de performance e/ou consumo excessivo de memória. Principalmente com grandes volumes de dados.
  3. Como resolver? O básico: - Seguir as convenções do Rails

    - Pensar em escalabilidade sempre - Adotar boas práticas ao criar consultas - Não aplicar a normalização quando necessário O avançado: Cache, Redis, Elasticsearch, PostgreSQL Materialized Views, etc.
  4. O Código da Aplicação Está disponível no Github. Antes: git

    clone [email protected]:nielsonrolim/ar_good_practices_before.git Depois: git clone [email protected]:nielsonrolim/ar_good_practices_after.git
  5. Pegue só o que for usar O método all é

    o default para todas consultas. Ele carrega todos os dados do objeto para memória: @orders = Order.all SELECT "orders".* FROM "orders" Usando o método select, podemos selecionar apenas o que precisamos: @orders = Order.select(:id, :created_at, :user_id) SELECT "orders"."id", "orders"."created_at", "orders"."user_id" FROM "orders"
  6. Counter Cache Tomando como exemplo os models Order e OrderItems.

    E se quisermos saber quantos itens existem em um pedido? Geralmente simplesmente usamos: my_order.order_items.length Ou my_order.order_items.count
  7. Counter Cache length é um método da classe Array. my_order.order_items.length

    SELECT "order_items".* FROM "order_items" WHERE "order_items"."order_id" = $1 A linha acima irá recuperar todos os registros de order_items associados ao pedido my_order e carregá-los em memória em um array. Somente após montado o array, o método length será executado retornando a quantidade de order_items.
  8. Counter Cache my_order.order_items.count SELECT COUNT("order_items".*) FROM "order_items" WHERE "order_items"."order_id" =

    $1 O método count é mais eficiente, pois é um método de ActiveRecord. Porém, uma consulta ao banco de dados ainda é executada.
  9. Counter Cache Com o Couter Cache esse problema é resolvido

    adicionando uma coluna na tabela orders para a quantidade de order_items: $ rails g migration add_order_items_count_to_orders order_items_count:integer $ rails db:migrate Além disso, é preciso adicionar o counter_cache na associação em OrderItem: class OrderItem < ApplicationRecord belongs_to :order, counter_cache: true end
  10. Counter Cache <%= my_order.order_items_count %> Agora, temos o atributo order_items_count

    em Order que guarda a quantidade de order_items existente. Esse atributo é atualizado automaticamente sempre que um order_item é adicionado ou removido de um objeto order.
  11. Eager Loading O pedido (Order) possui um usuário (User) associado.

    class Order < ApplicationRecord belongs_to :user end class User < ApplicationRecord has_many :orders end
  12. Eager Loading Para exibir o nome do usuário na listagem

    de pedidos: <%= order.user.name %> Dessa maneira, em cada linha da listagem de pedidos, será executada uma consulta ao banco de dados para recuperar os dados do usuário, montar o objeto User na memória, e então acessar o valor de name: SELECT "users".* FROM "users" WHERE "users"."id" = $1
  13. Para resolver esse problema, usamos o Eager Loading, ou Carregamento

    Ansioso (?). A maneira mais fácil de usar o Eager Loading é com o método includes: @orders = Order.includes(:user).select(:id, :created_at, :order_items_count, :user_id) Assim, os dados dos usuários são logo carregados em uma consulta: SELECT "orders"."id", "orders"."created_at", "orders"."order_items_count", "orders"."user_id" FROM "orders" ORDER BY "orders"."created_at" ASC SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 5, 3, 4) Eager Loading
  14. Contudo, porém, todavia, entretanto… Ainda são executadas duas consultas ao

    banco dados (uma para orders e outra para users). Utilizando o método joins, apenas uma consulta é feita: @orders = Order.joins(:user).select(:id, :created_at, :order_items_count, :user_id, 'users.name as user_name') SELECT "orders"."id", "orders"."created_at", "orders"."order_items_count", "orders"."user_id", users.name as user_name FROM "orders" INNER JOIN "users" ON "users"."id" = "orders"."user_id" ORDER BY "orders"."created_at" ASC Eager Loading
  15. Agora podemos acessar o nome do usuário através do atributo

    user_name: <%= order.user_name %> Podemos criar um método em Order para possibilitar o acesso a user_name, mesmo quando não estamos usando o Eager Loading: def user_name read_attribute('user_name') || user.name end Eager Loading
  16. Outra situação bastante comum é quando temos uma coleção e

    queremos excluir os objetos duplicados: my_order.products.uniq SELECT "products".* FROM "products" INNER JOIN "order_items" ON "products"."id" = "order_items"."product_id" WHERE "order_items"."order_id" = $1 Assim como length, uniq também é um método da classe Array. Logo, a linha acima irá recuperar do banco de dados todos os produtos do pedido, carregá-los na memória e só então executar o método uniq para eliminar os objetos duplicados. Esse procedimento consome muita memória. UNIQ vs DISTINCT
  17. Temos uma melhor performance utilizando o método distinct do ActiveRecord:

    Product.select(:name).distinct.joins(:orders).where(orders: {id: my_order.id}) SELECT DISTINCT "products"."name" FROM "products" INNER JOIN "order_items" ON "order_items"."product_id" = "products"."id" INNER JOIN "orders" ON "orders"."id" = "order_items"."order_id" WHERE "orders"."id" = $1 A consulta acima utiliza o DISTINCT na query SQL e retorna apenas os nomes do produtos do pedido, sem duplicados. O uso de memória é bem menor dessa maneira. Nota: Será retornada uma coleção de objetos do tipo Product UNIQ vs DISTINCT
  18. Mais dois exemplos de funções SQL que não são comumente

    utilizados em associações do ActiveRecord são SUM e AVG. Cálcuando a soma com sum da classe Array: self.order_items.sum(&:value) Calculando a média com avg da classe Array: self.order_items.sum(&:value) / self.order_items.size Mais uma vez, temos um uso desnecessário de memória, pois estamos carregando todos os objetos em memória e depois executando as operações. SUM & AVG
  19. Para obter o mesmo resultado de forma mais eficiente, podemos

    utilizar os métodos do ActiveRecord sum e average: Soma: OrderItem.where(order: my_order).sum(:value) SELECT SUM("order_items"."value") FROM "order_items" WHERE "order_items"."order_id" = 1 Média: OrderItem.where(order: my_order).average(:value) SELECT AVG("order_items"."value") FROM "order_items" WHERE "order_items"."order_id" = 1 SUM & AVG
  20. É possível executar uma query SQL “crua”: Product.find_by_sql(["select distinct products.name

    from products inner join order_items on order_items.product_id = products.id inner join orders on orders.id = order_items.order_id where orders.id = ?", my_order.id]) A linha acima irá retornar uma coleção de produtos, apenas com os nomes preenchidos, associados ao pedido my_order, sem duplicados. Da mesma maneira como mostrado anteriormente, porém utilizando uma query SQL “crua”. Plain SQL ou quase
  21. Também é possível executar funções SQL utilizando o padrão de

    consultas do ActiveRecord: OrderItem.select("sum(order_items.value) as sum_value").where(order: self).take.sum_value OrderItem.select("avg(order_items.value) as avg_value").where(order: self).take.avg_value O resultado é o mesmo do que foi mostrado anteriormente utilizando os métodos sum e avg do ActiveRecord. Plain SQL ou quase
  22. - O Rails possui os métodos find_each e find_in_batches para

    processamento em lote; - O método reorder sobreescreve o order original de uma consulta. Útil quando se utiliza default_scope para ordenação padrão em um model; - No Rails 5 existe o método not para negação de condições; - O Rails 5 também trouxe o método left_outer_joins para gerar consultas com LEFT JOINS. Até o Rails 4, isso só era possível utilizando SQL crua e o método find_by_sql. Também é bom saber
  23. - RailsGuide - http://guides.rubyonrails.org - http://guides.rubyonrails.org/active_record_basics.html - http://guides.rubyonrails.org/active_record_querying.html - http://guides.rubyonrails.org/active_record_validations.html

    - http://guides.rubyonrails.org/active_record_callbacks.html - RailsCasts - http://railscasts.com - https://evilmartians.com/chronicles/5-tips-for-activerecord-dashboards - https://blog.codeship.com/speed-up-activerecord/ - https://www.webascender.com/blog/rails-tips-speeding-activerecord-quer ies/ Referências