perf(dossier): improuve dossier preloading perf

This commit is contained in:
Paul Chavard 2022-06-13 21:54:57 +01:00
parent b6868ce9ea
commit 564daeffe8
5 changed files with 109 additions and 35 deletions

View file

@ -49,6 +49,7 @@ class Champ < ApplicationRecord
:dossier_link?, :dossier_link?,
:titre_identite?, :titre_identite?,
:header_section?, :header_section?,
:siret?,
:stable_id, :stable_id,
to: :type_de_champ to: :type_de_champ
@ -76,6 +77,10 @@ class Champ < ApplicationRecord
!private? !private?
end end
def child?
parent_id.present?
end
def sections def sections
@sections ||= dossier&.sections_for(self) @sections ||= dossier&.sections_for(self)
end end

View file

@ -250,6 +250,7 @@ class Dossier < ApplicationRecord
:followers_instructeurs, :followers_instructeurs,
:traitement, :traitement,
:groupe_instructeur, :groupe_instructeur,
:etablissement,
procedure: [ procedure: [
:groupe_instructeurs, :groupe_instructeurs,
:draft_types_de_champ, :draft_types_de_champ,
@ -257,8 +258,7 @@ class Dossier < ApplicationRecord
:published_types_de_champ, :published_types_de_champ,
:published_types_de_champ_private :published_types_de_champ_private
], ],
avis: [:claimant, :expert], avis: [:claimant, :expert]
etablissement: :champ
).order(depose_at: 'asc') ).order(depose_at: 'asc')
} }
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction } scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
@ -435,39 +435,101 @@ class Dossier < ApplicationRecord
validates :individual, presence: true, if: -> { revision.procedure.for_individual? } validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
validates :groupe_instructeur, presence: true, if: -> { !brouillon? } validates :groupe_instructeur, presence: true, if: -> { !brouillon? }
EXPORT_BATCH_SIZE = 5000 EXPORT_BATCH_SIZE = 2000
def self.downloadable_sorted_batch def self.downloadable_sorted_batch
dossiers = downloadable_sorted.to_a ExportPreloader.new(self).in_batches
(dossiers.size.to_f / EXPORT_BATCH_SIZE).ceil.times do |i|
start_index = i * EXPORT_BATCH_SIZE
end_index = start_index + EXPORT_BATCH_SIZE - 1
load_champs(dossiers[start_index..end_index])
end 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 dossiers
end end
def self.load_champs(dossiers) private
::ActiveRecord::Associations::Preloader.new.preload(dossiers, {
champs: { # returns: { revision_id : { type_de_champ_id : position } }
type_de_champ: [], def positions
etablissement: :champ, @positions ||= ProcedureRevisionTypeDeChamp
piece_justificative_file_attachment: :blob, .where(revision_id: @dossiers.distinct.pluck(:revision_id))
champs: [ .select(:revision_id, :type_de_champ_id, :position)
type_de_champ: [], .group_by(&:revision_id)
piece_justificative_file_attachment: :blob .transform_values do |coordinates|
] coordinates.index_by(&:type_de_champ_id).transform_values(&:position)
}, end
champs_private: { end
type_de_champ: [],
etablissement: :champ, def load_dossiers(dossiers)
piece_justificative_file_attachment: :blob, all_champs = Champ
champs: [ .includes(:type_de_champ, piece_justificative_file_attachment: :blob)
type_de_champ: [], .where(dossier_id: dossiers)
piece_justificative_file_attachment: :blob .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
end end
def user_deleted? def user_deleted?

View file

@ -203,6 +203,10 @@ class TypeDeChamp < ApplicationRecord
type_champ == TypeDeChamp.type_champs.fetch(:dossier_link) type_champ == TypeDeChamp.type_champs.fetch(:dossier_link)
end end
def siret?
type_champ == TypeDeChamp.type_champs.fetch(:siret)
end
def piece_justificative? def piece_justificative?
type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) || type_champ == TypeDeChamp.type_champs.fetch(:titre_identite) type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) || type_champ == TypeDeChamp.type_champs.fetch(:titre_identite)
end end

View file

@ -4,7 +4,6 @@ class ProcedureExportService
def initialize(procedure, dossiers) def initialize(procedure, dossiers)
@procedure = procedure @procedure = procedure
@dossiers = dossiers @dossiers = dossiers
@tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
end end
def to_csv def to_csv
@ -15,8 +14,10 @@ class ProcedureExportService
def to_xlsx def to_xlsx
@dossiers = @dossiers.downloadable_sorted_batch @dossiers = @dossiers.downloadable_sorted_batch
tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
# We recursively build multi page spreadsheet # We recursively build multi page spreadsheet
io = @tables.reduce(nil) do |package, table| io = tables.reduce(nil) do |package, table|
SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package) SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package)
end.to_stream end.to_stream
create_blob(io, :xlsx) create_blob(io, :xlsx)
@ -24,8 +25,10 @@ class ProcedureExportService
def to_ods def to_ods
@dossiers = @dossiers.downloadable_sorted_batch @dossiers = @dossiers.downloadable_sorted_batch
tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
# We recursively build multi page spreadsheet # We recursively build multi page spreadsheet
io = StringIO.new(@tables.reduce(nil) do |spreadsheet, table| io = StringIO.new(tables.reduce(nil) do |spreadsheet, table|
SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet) SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet)
end.bytes) end.bytes)
create_blob(io, :ods) create_blob(io, :ods)

View file

@ -4,7 +4,7 @@ describe 'Instructing a dossier:', js: true do
let(:password) { 'my-s3cure-p4ssword' } let(:password) { 'my-s3cure-p4ssword' }
let!(:instructeur) { create(:instructeur, password: password) } let!(:instructeur) { create(:instructeur, password: password) }
let!(:procedure) { create(:procedure, :published, instructeurs: [instructeur]) } let!(:procedure) { create(:procedure, :with_type_de_champ, :published, instructeurs: [instructeur]) }
let!(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) } let!(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) }
context 'the instructeur is also a user' do context 'the instructeur is also a user' do
scenario 'a instructeur can fill a dossier' do scenario 'a instructeur can fill a dossier' do