Skip to content

Commit 294c7c0

Browse files
committed
Add bulk inserts
1 parent cd17fda commit 294c7c0

1 file changed

Lines changed: 132 additions & 36 deletions

File tree

app/services/charge_filters/create_or_update_batch_service.rb

Lines changed: 132 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def call
3838
# it on every built/looked-up child.
3939
organization = charge.organization
4040

41+
new_filter_rows = []
42+
new_filter_value_rows = []
43+
new_filter_ids_in_order = []
44+
4145
filters_params.each do |filter_param|
4246
# NOTE: callers pass either string-keyed (plan flow, via with_indifferent_access) or
4347
# symbol-keyed (subscription override flow, via deep_symbolize_keys) values
@@ -46,50 +50,27 @@ def call
4650

4751
# NOTE: since a filter could be a refinement of another one, we have to make sure
4852
# that we are targeting the right one
49-
filter = filters_by_values_key[values_params.sort]
50-
matched_existing_filter = !filter.nil?
51-
52-
filter ||= charge.filters.new(organization_id: charge.organization_id)
53-
filter.charge = charge
54-
filter.organization = organization
53+
existing_filter = filters_by_values_key[values_params.sort]
5554

56-
filter.invoice_display_name = filter_param[:invoice_display_name]
57-
filter.properties = ChargeModels::FilterPropertiesService.call(
55+
properties = ChargeModels::FilterPropertiesService.call(
5856
chargeable: charge,
5957
properties: filter_param[:properties]&.deep_symbolize_keys&.except(:presentation_group_keys)
6058
).properties
6159

62-
filter.save! if filter.changed?
63-
64-
# NOTE: Make sure updated_at is touched even if not changed to keep the right order.
65-
filter.touch if touch # rubocop:disable Rails/SkipsModelValidations
66-
67-
# NOTE: Create or update the filter values
68-
values_params.each do |key, values|
69-
billable_metric_filter = billable_metric_filters_by_key[key]
70-
71-
# NOTE: only look up an existing filter_value if the parent filter came from
72-
# the preloaded set. For freshly-created filters, the values collection
73-
# is provably empty — querying it on a now-persisted record issues a
74-
# wasted SELECT per filter.
75-
filter_value = if matched_existing_filter
76-
filter.values.to_a.find { |v| v.billable_metric_filter_id == billable_metric_filter&.id }
77-
end
78-
filter_value ||= filter.values.build(organization_id: charge.organization_id)
79-
filter_value.charge_filter = filter
80-
filter_value.billable_metric_filter = billable_metric_filter
81-
filter_value.organization = organization
82-
83-
filter_value.values = values
84-
filter_value.save! if filter_value.changed?
85-
86-
# NOTE: Make sure updated_at is touched even if not changed to keep the right order.
87-
filter_value.touch if touch # rubocop:disable Rails/SkipsModelValidations
60+
if existing_filter
61+
update_existing_filter(
62+
existing_filter, organization, filter_param, values_params, properties, touch
63+
)
64+
else
65+
accumulate_new_filter(
66+
organization, filter_param, values_params, properties,
67+
new_filter_rows, new_filter_value_rows, new_filter_ids_in_order
68+
)
8869
end
89-
90-
result.filters << filter
9170
end
9271

72+
bulk_insert_new_filters(new_filter_rows, new_filter_value_rows, new_filter_ids_in_order)
73+
9374
# NOTE: remove old filters that were not created or updated
9475
charge.filters.where.not(id: result.filters.map(&:id)).unscope(:order).find_each do
9576
remove_filter(it)
@@ -107,6 +88,121 @@ def call
10788

10889
attr_reader :charge, :filters_params
10990

91+
def update_existing_filter(filter, organization, filter_param, values_params, properties, touch)
92+
filter.charge = charge
93+
filter.organization = organization
94+
95+
filter.invoice_display_name = filter_param[:invoice_display_name]
96+
filter.properties = properties
97+
98+
filter.save! if filter.changed?
99+
100+
# NOTE: Make sure updated_at is touched even if not changed to keep the right order.
101+
filter.touch if touch # rubocop:disable Rails/SkipsModelValidations
102+
103+
values_params.each do |key, values|
104+
billable_metric_filter = billable_metric_filters_by_key[key]
105+
106+
# NOTE: existing filter was preloaded with values, so this in-memory find avoids a SELECT.
107+
filter_value = filter.values.to_a.find { |v| v.billable_metric_filter_id == billable_metric_filter&.id }
108+
filter_value ||= filter.values.build(organization_id: charge.organization_id)
109+
filter_value.charge_filter = filter
110+
filter_value.billable_metric_filter = billable_metric_filter
111+
filter_value.organization = organization
112+
113+
filter_value.values = values
114+
filter_value.save! if filter_value.changed?
115+
116+
filter_value.touch if touch # rubocop:disable Rails/SkipsModelValidations
117+
end
118+
119+
result.filters << filter
120+
end
121+
122+
def accumulate_new_filter(organization, filter_param, values_params, properties,
123+
new_filter_rows, new_filter_value_rows, new_filter_ids_in_order)
124+
# NOTE: pre-generate the UUID so we can wire ChargeFilterValue rows to their parent
125+
# without a round-trip after the ChargeFilter insert_all.
126+
filter_id = SecureRandom.uuid
127+
128+
# NOTE: build an in-memory AR instance only to run validations — we drop it after
129+
# capturing the row hash. Use a standalone ChargeFilter.new (not
130+
# `charge.filters.new`) so the unsaved instance is not appended to the
131+
# parent's in-memory collection, which can interfere with later charge.save!
132+
# in callers like Charges::CreateService.
133+
filter_instance = ChargeFilter.new(
134+
id: filter_id,
135+
charge_id: charge.id,
136+
organization_id: charge.organization_id,
137+
invoice_display_name: filter_param[:invoice_display_name],
138+
properties: properties
139+
)
140+
filter_instance.charge = charge
141+
filter_instance.organization = organization
142+
filter_instance.validate!
143+
144+
new_filter_rows << {
145+
id: filter_id,
146+
charge_id: charge.id,
147+
organization_id: charge.organization_id,
148+
invoice_display_name: filter_param[:invoice_display_name],
149+
properties: properties
150+
}
151+
new_filter_ids_in_order << filter_id
152+
153+
values_params.each do |key, values|
154+
billable_metric_filter = billable_metric_filters_by_key[key]
155+
156+
value_instance = ChargeFilterValue.new(
157+
organization_id: charge.organization_id,
158+
values: values
159+
)
160+
value_instance.charge_filter = filter_instance
161+
value_instance.billable_metric_filter = billable_metric_filter
162+
value_instance.organization = organization
163+
value_instance.validate!
164+
165+
new_filter_value_rows << {
166+
charge_filter_id: filter_id,
167+
billable_metric_filter_id: billable_metric_filter&.id,
168+
organization_id: charge.organization_id,
169+
values: values
170+
}
171+
end
172+
end
173+
174+
def bulk_insert_new_filters(new_filter_rows, new_filter_value_rows, new_filter_ids_in_order)
175+
return if new_filter_rows.empty?
176+
177+
# NOTE: insert_all skips AR callbacks (and therefore updated_at handling), so set
178+
# monotonically-increasing timestamps to preserve the request order under the
179+
# model's `order(updated_at: :asc)` default scope.
180+
now = Time.current
181+
new_filter_rows.each_with_index do |row, idx|
182+
ts = now + (idx / 1_000_000.0)
183+
row[:created_at] = ts
184+
row[:updated_at] = ts
185+
end
186+
ChargeFilter.insert_all!(new_filter_rows)
187+
188+
if new_filter_value_rows.any?
189+
new_filter_value_rows.each_with_index do |row, idx|
190+
ts = now + (idx / 1_000_000.0)
191+
row[:created_at] = ts
192+
row[:updated_at] = ts
193+
end
194+
ChargeFilterValue.insert_all!(new_filter_value_rows)
195+
end
196+
197+
# NOTE: re-fetch the inserted filters with their values so callers iterating over
198+
# result.filters see fully-hydrated records.
199+
inserted = charge.filters
200+
.where(id: new_filter_ids_in_order)
201+
.includes(values: :billable_metric_filter)
202+
.index_by(&:id)
203+
new_filter_ids_in_order.each { |id| result.filters << inserted[id] }
204+
end
205+
110206
def filters
111207
@filters ||= charge.filters.includes(values: :billable_metric_filter)
112208
end

0 commit comments

Comments
 (0)