From 042703cead3e7a497820a1ffa1e3325e913c7d63 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 20 Jul 2022 19:35:34 +0200 Subject: [PATCH] refactor(dossier): extract dossier preloader --- app/models/dossier.rb | 103 +------------------------------- app/models/dossier_preloader.rb | 92 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 101 deletions(-) create mode 100644 app/models/dossier_preloader.rb diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 45b55f0e3..4e5713d35 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -251,13 +251,7 @@ class Dossier < ApplicationRecord :traitement, :groupe_instructeur, :etablissement, - procedure: [ - :groupe_instructeurs, - :draft_types_de_champ, - :draft_types_de_champ_private, - :published_types_de_champ, - :published_types_de_champ_private - ], + procedure: [:groupe_instructeurs], avis: [:claimant, :expert] ).order(depose_at: 'asc') } @@ -439,101 +433,8 @@ class Dossier < ApplicationRecord types_de_champ end - EXPORT_BATCH_SIZE = 2000 - def self.downloadable_sorted_batch - ExportPreloader.new(self).in_batches - end - - class ExportPreloader - def initialize(dossiers) - @dossiers = dossiers - end - - def in_batches - dossiers = @dossiers.downloadable_sorted.to_a - dossiers.each_slice(EXPORT_BATCH_SIZE) { |slice| load_dossiers(slice) } - dossiers - end - - private - - # returns: { revision_id : { type_de_champ_id : position } } - def positions - @positions ||= ProcedureRevisionTypeDeChamp - .where(revision_id: @dossiers.distinct.pluck(:revision_id)) - .select(:revision_id, :type_de_champ_id, :position) - .group_by(&:revision_id) - .transform_values do |coordinates| - coordinates.index_by(&:type_de_champ_id).transform_values(&:position) - end - end - - def load_dossiers(dossiers) - all_champs = Champ - .includes(:type_de_champ, piece_justificative_file_attachment: :blob) - .where(dossier_id: dossiers) - .to_a - - load_etablissements(all_champs) - - children_champs, root_champs = all_champs.partition(&:child?) - champs_by_dossier = root_champs.group_by(&:dossier_id) - champs_by_dossier_by_parent = children_champs - .group_by(&:dossier_id) - .transform_values do |champs| - champs.group_by(&:parent_id) - end - - dossiers.each do |dossier| - load_dossier(dossier, champs_by_dossier[dossier.id], champs_by_dossier_by_parent[dossier.id] || {}) - end - end - - def load_etablissements(champs) - champs_siret = champs.filter(&:siret?) - etablissements_by_id = Etablissement.where(id: champs_siret.map(&:etablissement_id).compact).index_by(&:id) - champs_siret.each do |champ| - etablissement = etablissements_by_id[champ.etablissement_id] - champ.association(:etablissement).target = etablissement - if etablissement - etablissement.association(:champ).target = champ - end - end - end - - def load_dossier(dossier, champs, children_by_parent = {}) - champs_public, champs_private = champs.partition(&:public?) - - load_champs(dossier, :champs, champs_public, dossier) - load_champs(dossier, :champs_private, champs_private, dossier) - - # Load repetition children champs - champs.filter(&:repetition?).each do |parent_champ| - champs = children_by_parent[parent_champ.id] || [] - parent_champ.association(:dossier).target = dossier - - load_champs(parent_champ, :champs, champs, dossier) - parent_champ.association(:champs).set_inverse_instance(parent_champ) - end - - # We need to do this because of the check on `Etablissement#champ` in - # `Etablissement#libelle_for_export`. By assigning `nil` to `target` we mark association - # as loaded and so the check on `Etablissement#champ` will not trigger n+1 query. - if dossier.etablissement - dossier.etablissement.association(:champ).target = nil - end - end - - def load_champs(parent, name, champs, dossier) - champs.each do |champ| - champ.association(:dossier).target = dossier - end - - parent.association(name).target = champs.sort_by do |champ| - positions[dossier.revision_id][champ.type_de_champ_id] - end - end + DossierPreloader.new(downloadable_sorted).in_batches end def user_deleted? diff --git a/app/models/dossier_preloader.rb b/app/models/dossier_preloader.rb new file mode 100644 index 000000000..86daf4af0 --- /dev/null +++ b/app/models/dossier_preloader.rb @@ -0,0 +1,92 @@ +class DossierPreloader + DEFAULT_BATCH_SIZE = 2000 + + def initialize(dossiers) + @dossiers = dossiers + end + + def in_batches(size = DEFAULT_BATCH_SIZE) + dossiers = @dossiers.to_a + dossiers.each_slice(size) { |slice| load_dossiers(slice) } + dossiers + end + + private + + # returns: { revision_id : { type_de_champ_id : position } } + def positions + @positions ||= ProcedureRevisionTypeDeChamp + .where(revision_id: @dossiers.pluck(:revision_id).uniq) + .select(:revision_id, :type_de_champ_id, :position) + .group_by(&:revision_id) + .transform_values do |coordinates| + coordinates.index_by(&:type_de_champ_id).transform_values(&:position) + end + end + + def load_dossiers(dossiers) + all_champs = Champ + .includes(:type_de_champ, piece_justificative_file_attachment: :blob) + .where(dossier_id: dossiers) + .to_a + + load_etablissements(all_champs) + + children_champs, root_champs = all_champs.partition(&:child?) + champs_by_dossier = root_champs.group_by(&:dossier_id) + champs_by_dossier_by_parent = children_champs + .group_by(&:dossier_id) + .transform_values do |champs| + champs.group_by(&:parent_id) + end + + dossiers.each do |dossier| + load_dossier(dossier, champs_by_dossier[dossier.id] || [], champs_by_dossier_by_parent[dossier.id] || {}) + end + end + + def load_etablissements(champs) + champs_siret = champs.filter(&:siret?) + etablissements_by_id = Etablissement.where(id: champs_siret.map(&:etablissement_id).compact).index_by(&:id) + champs_siret.each do |champ| + etablissement = etablissements_by_id[champ.etablissement_id] + champ.association(:etablissement).target = etablissement + if etablissement + etablissement.association(:champ).target = champ + end + end + end + + def load_dossier(dossier, champs, children_by_parent = {}) + champs_public, champs_private = champs.partition(&:public?) + + load_champs(dossier, :champs, champs_public, dossier, children_by_parent) + load_champs(dossier, :champs_private, champs_private, dossier, children_by_parent) + + # We need to do this because of the check on `Etablissement#champ` in + # `Etablissement#libelle_for_export`. By assigning `nil` to `target` we mark association + # as loaded and so the check on `Etablissement#champ` will not trigger n+1 query. + if dossier.etablissement + dossier.etablissement.association(:champ).target = nil + end + end + + def load_champs(parent, name, champs, dossier, children_by_parent) + champs.each do |champ| + champ.association(:dossier).target = dossier + end + + parent.association(name).target = champs.sort_by do |champ| + positions[dossier.revision_id][champ.type_de_champ_id] + end + + # Load children champs + champs.filter(&:repetition?).each do |parent_champ| + champs = children_by_parent[parent_champ.id] || [] + parent_champ.association(:dossier).target = dossier + + load_champs(parent_champ, :champs, champs, dossier, children_by_parent) + parent_champ.association(:champs).set_inverse_instance(parent_champ) + end + end +end