Skip to content

Commit

Permalink
Speed-up adding new models to has-many association scope
Browse files Browse the repository at this point in the history
  • Loading branch information
y9v committed Nov 7, 2017
1 parent 2e29f0c commit a95f940
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 12 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,9 @@ end

#### Performance

In general you are safe to use JSONB HABTM for models that won't have many associations on both sides.
Compared to regular associations, fetching models associated via JSONB column has no drops in performance.

If you plan to have associations where one side has many records on the other side (more then ~500), adding new records to existing associations scope will be slower then with traditional approach.

Below you can see benchmark results for adding new associations to the association scope:
Adding new connections is slightly faster with JSONB, for scopes up to 500 records connected to another record (total count of records in the table does not matter that much. If you have more then ~500 records connected to one record on average, and you want to add new records to the scope, JSONB associations will be slower then traditional:

<img src="https://github.com/lebedev-yury/activerecord-jsonb-associations/blob/master/doc/images/adding-associations.png?raw=true | width=500" alt="JSONB HAMTB is slower on adding associations" width="600">

Expand Down
52 changes: 51 additions & 1 deletion benchmarks/habtm.rake
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ require 'benchmark/ips'
namespace :benchmarks do
desc 'Regular vs JSONB HABTM benchmarks'
task :habtm do
(10..5_010).step(500).to_a.each do |associations_count|
[
10, 100, 500, 1000, 2_500, 5_000, 10_000
].to_a.each do |associations_count|
user = User.create
user_with_groups_only = User.create
user_with_labels_only = User.create
Expand All @@ -20,6 +22,54 @@ namespace :benchmarks do
associations_count,
users: [user, user_with_labels_only]

Benchmark.ips do |x|
x.config(warmup: 0)

x.report(
"Regular: fetching associations with #{associations_count} existing"
) do
ActiveRecord::Base.transaction do
Group.uncached { user.groups.reload }
raise ActiveRecord::Rollback
end
end

x.report(
"JSONB: fetching associations with #{associations_count} existing"
) do
ActiveRecord::Base.transaction do
Label.uncached { user.labels.reload }
raise ActiveRecord::Rollback
end
end

x.compare!
end

Benchmark.ips do |x|
x.config(warmup: 0)

x.report(
"Regular: getting association ids with #{associations_count} existing"
) do
ActiveRecord::Base.transaction do
Group.uncached { user.group_ids }
raise ActiveRecord::Rollback
end
end

x.report(
"JSONB: getting association ids with #{associations_count} existing"
) do
ActiveRecord::Base.transaction do
Label.uncached { user.label_ids }
raise ActiveRecord::Rollback
end
end

x.compare!
end

Benchmark.ips do |x|
x.config(warmup: 0)

Expand Down
Binary file modified doc/images/adding-associations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified doc/images/deleting-associations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 13 additions & 7 deletions lib/activerecord/jsonb/associations/has_many_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,25 @@ def create_scope
end
# rubocop:enable Metrics/AbcSize

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
def insert_record(record, validate = true, raise = false)
super.tap do
super.tap do |super_result|
next unless options.key?(:store)
next unless super_result

key = "#{record.model_name.singular}_ids"
store = owner[options[:store]] || {}
store[key] ||= []
store[key] << record.id
owner.update_column(options[:store], store)
jsonb_column = options[:store]

owner.class.where(
owner.class.primary_key => owner[owner.class.primary_key]
).update_all(%(
#{jsonb_column} = jsonb_set(#{jsonb_column}, '{#{key}}',
coalesce(#{jsonb_column}->'#{key}', '[]'::jsonb) ||
'[#{record[klass.primary_key]}]'::jsonb)
))
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize

def delete_records(records, method)
return super unless options.key?(:store)
Expand Down

0 comments on commit a95f940

Please sign in to comment.