Skip to content

Commit

Permalink
refactor gem to be compatiable for multiple versions; adding ci
Browse files Browse the repository at this point in the history
  • Loading branch information
wendy-clio committed Apr 22, 2024
1 parent c7ca2a9 commit 24ead71
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 15 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
gemfile:
- Gemfile
- Gemfile.5.2
- Gemfile.6.0
- Gemfile.6.1
env:
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v4
- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- name: Install dependencies
run: bundle install
- name: Run tests
run:
bundle exec rspec
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/test/tmp/
/test/version_tmp/
/tmp/
.idea/*

# Used by dotenv library to load environment variables.
# .env
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
source 'https://rubygems.org'

gem "activerecord", ">=7"
# Specify your gem's dependencies in jit_preloader.gemspec
gemspec
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,20 @@ end

```

Furthermore, there is an argument `max_ids_per_query` setting max ids per query. This helps prevent running a single query with too large list of ids which may be less efficient than splitting into multiple queries.
```ruby
class Contact < ActiveRecord::Base
has_many :addresses
has_many_aggregate :addresses, :count_all, :count, "*", max_ids_per_query: 10
end

Contact.jit_preload.each do |contact|
contact.addresses_count_all
end
# SELECT contact_id, COUNT(*) FROM addresses WHERE contact_id IN (1, 2, 3, ... ,10) GROUP BY contact_id
# SELECT contact_id, COUNT(*) FROM addresses WHERE contact_id IN (11, 12, 13) GROUP BY contact_id
```

### Preloading a subset of an association

There are often times when you want to preload a subset of an association, or change how the SQL statement is generated. For example, if a `Contact` model has
Expand Down Expand Up @@ -213,6 +227,7 @@ end
### Jit preloading globally across your application

The JitPreloader can be globally enabled, in which case most N+1 queries in your app should just disappear. It is off by default.
The `max_ids_per_query` argument on loading aggregate methods can also apply on a global level.

```ruby
# Can be true or false
Expand All @@ -223,12 +238,24 @@ JitPreloader.globally_enabled = true
# so that you can turn it on or off dynamically.
JitPreloader.globally_enabled = ->{ $redis.get('always_jit_preload') == 'on' }

# Setting global max ids constraint on all aggregation methods.
JitPreloader.max_ids_per_query = 10

class Contact < ActiveRecord::Base
has_many :emails
has_many_aggregate :emails, :count_all, :count, "*"
end

# When enabled globally, this would not generate an N+1 query.
Contact.all.each do |contact|
contact.emails.each do |email|
# do something
end
# When max_ides_per_query is set globally, the aggregate method will split query base on the limit.
contact.emails_count_all
end


```

## What it doesn't solve
Expand Down
2 changes: 1 addition & 1 deletion jit_preloader.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]

spec.add_dependency "activerecord", ">= 7", "< 8"
spec.add_dependency "activerecord", "< 8"
spec.add_dependency "activesupport"

spec.add_development_dependency "bundler"
Expand Down
2 changes: 1 addition & 1 deletion lib/jit_preloader/active_record/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def clear_jit_preloader!
end
end

if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.0.0")
def preload_scoped_relation(name:, base_association:, preload_scope: nil)
return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)

Expand Down
46 changes: 34 additions & 12 deletions lib/jit_preloader/preloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,47 @@ class Preloader < ActiveRecord::Associations::Preloader

attr_accessor :records

def self.attach(records)
new(records: records.dup, associations: nil).tap do |loader|
records.each do |record|
record.jit_preloader = loader
if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.0.0")
def self.attach(records)
new(records: records.dup, associations: nil).tap do |loader|
records.each do |record|
record.jit_preloader = loader
end
end
end
end

def jit_preload(associations)
# It is possible that the records array has multiple different classes (think single table inheritance).
# Thus, it is possible that some of the records don't have an association.
records_with_association = records.reject{|r| r.class.reflect_on_association(associations).nil? }
def jit_preload(associations)
# It is possible that the records array has multiple different classes (think single table inheritance).
# Thus, it is possible that some of the records don't have an association.
records_with_association = records.reject{|r| r.class.reflect_on_association(associations).nil? }

# Some of the records may already have the association loaded and we should not load them again
records_requiring_loading = records_with_association.select{|r| !r.association(associations).loaded? }

self.class.new(records: records_requiring_loading, associations: associations).call
end
else
def self.attach(records)
new.tap do |loader|
loader.records = records.dup
records.each do |record|
record.jit_preloader = loader
end
end
end

# Some of the records may already have the association loaded and we should not load them again
records_requiring_loading = records_with_association.select{|r| !r.association(associations).loaded? }
def jit_preload(associations)
# It is possible that the records array has multiple different classes (think single table inheritance).
# Thus, it is possible that some of the records don't have an association.
records_with_association = records.reject{ |record| record.class.reflect_on_association(associations).nil? }

self.class.new(records: records_requiring_loading, associations: associations).call
# Some of the records may already have the association loaded and we should not load them again
records_requiring_loading = records_with_association.select{ |record| !record.association(associations).loaded? }
preload records_with_association, associations
end
end


# We do not want the jit_preloader to be dumpable
# If you dump a ActiveRecord::Base object that has a jit_preloader instance variable
# you will also end up dumping all of the records the preloader has reference to.
Expand Down
2 changes: 1 addition & 1 deletion lib/jit_preloader/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module JitPreloader
VERSION = "2.0.2"
VERSION = "2.1.0"
end

0 comments on commit 24ead71

Please sign in to comment.