mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add has_one through disable_joins
This is similar to the `disable_joins` option on `has_many :through` associations applied to `has_one :through` associations. When `disable_joins` is set Rails will create 2 or more queries to get associations instead of generating a join. ```ruby class Person belongs_to :dog has_one :veterinarian, through: :dog, disable_joins: true end ``` Then instead of generating join SQL, two queries are used for `@person.veterinarian`: ``` SELECT "dogs"."id" FROM "dogs" WHERE "dogs"."person_id" = ? [["person_id", 1]] SELECT "veterinarians".* FROM "veterinarians" WHERE "veterinarians"."dog_id" = ? [["dog_id", 1]] ``` Co-authored-by: Eileen M. Uchitelle <eileencodes@gmail.com>
This commit is contained in:
parent
d89d14d241
commit
f7d7a22d01
7 changed files with 109 additions and 7 deletions
|
@ -1,3 +1,27 @@
|
|||
* Add option to disable joins for `has_one` associations.
|
||||
|
||||
In a multiple database application, associations can't join across
|
||||
databases. When set, this option instructs Rails to generate 2 or
|
||||
more queries rather than generating joins for `has_one` associations.
|
||||
|
||||
Set the option on a has one through association:
|
||||
|
||||
```ruby
|
||||
class Person
|
||||
belongs_to :dog
|
||||
has_one :veterinarian, through: :dog, disable_joins: true
|
||||
end
|
||||
```
|
||||
|
||||
Then instead of generating join SQL, two queries are used for `@person.veterinarian`:
|
||||
|
||||
```
|
||||
SELECT "dogs"."id" FROM "dogs" WHERE "dogs"."person_id" = ? [["person_id", 1]]
|
||||
SELECT "veterinarians".* FROM "veterinarians" WHERE "veterinarians"."dog_id" = ? [["dog_id", 1]]
|
||||
```
|
||||
|
||||
*Sarah Vessels*, *Eileen M. Uchitelle*
|
||||
|
||||
* `Arel::Visitors::Dot` now renders a complete set of properties when visiting
|
||||
`Arel::Nodes::SelectCore`, `SelectStatement`, `InsertStatement`, `UpdateStatement`, and
|
||||
`DeleteStatement`, which fixes #42026. Previously, some properties were omitted.
|
||||
|
|
|
@ -1555,6 +1555,22 @@ module ActiveRecord
|
|||
#
|
||||
# If you are going to modify the association (rather than just read from it), then it is
|
||||
# a good idea to set the <tt>:inverse_of</tt> option.
|
||||
# [:disable_joins]
|
||||
# Specifies whether joins should be skipped for an association. If set to true, two or more queries
|
||||
# will be generated. Note that in some cases, if order or limit is applied, it will be done in-memory
|
||||
# due to database limitations. This option is only applicable on `has_one :through` associations as
|
||||
# `has_one` alone does not perform a join.
|
||||
#
|
||||
# If the association on the join model is a #belongs_to, the collection can be modified
|
||||
# and the records on the <tt>:through</tt> model will be automatically created and removed
|
||||
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
|
||||
# <tt>:through</tt> association directly.
|
||||
#
|
||||
# If you are going to modify the association (rather than just read from it), then it is
|
||||
# a good idea to set the <tt>:inverse_of</tt> option on the source association on the
|
||||
# join model. This allows associated records to be built which will automatically create
|
||||
# the appropriate join model records when they are saved. (See the 'Association Join Models'
|
||||
# section above.)
|
||||
# [:source]
|
||||
# Specifies the source association name used by #has_one <tt>:through</tt> queries.
|
||||
# Only use it if the name cannot be inferred from the association.
|
||||
|
@ -1596,6 +1612,7 @@ module ActiveRecord
|
|||
# has_one :attachment, as: :attachable
|
||||
# has_one :boss, -> { readonly }
|
||||
# has_one :club, through: :membership
|
||||
# has_one :club, through: :membership, disable_joins: true
|
||||
# has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
|
||||
# has_one :credit_card, required: true
|
||||
# has_one :credit_card, strict_loading: true
|
||||
|
|
|
@ -11,6 +11,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
valid += [:as, :foreign_type] if options[:as]
|
||||
valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
|
||||
valid += [:through, :source, :source_type] if options[:through]
|
||||
valid += [:disable_joins] if options[:disable_joins] && options[:through]
|
||||
valid
|
||||
end
|
||||
|
||||
|
|
|
@ -6,6 +6,12 @@ module ActiveRecord
|
|||
class HasOneThroughAssociation < HasOneAssociation #:nodoc:
|
||||
include ThroughAssociation
|
||||
|
||||
def find_target
|
||||
return scope.first if disable_joins
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
def replace(record, save = true)
|
||||
create_through_record(record, save)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cases/helper"
|
||||
require "models/member"
|
||||
require "models/organization"
|
||||
|
||||
class HasOneThroughDisableJoinsAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :members, :organizations
|
||||
|
||||
def setup
|
||||
@member = members(:groucho)
|
||||
@organization = organizations(:discordians)
|
||||
@member.organization = @organization
|
||||
@member.save!
|
||||
@member.reload
|
||||
end
|
||||
|
||||
def test_counting_on_disable_joins_through
|
||||
no_joins = capture_sql { @member.organization_without_joins }
|
||||
joins = capture_sql { @member.organization }
|
||||
|
||||
assert_equal @member.organization, @member.organization_without_joins
|
||||
assert_equal 2, no_joins.count
|
||||
assert_equal 1, joins.count
|
||||
assert_match(/INNER JOIN/, joins.first)
|
||||
no_joins.each do |nj|
|
||||
assert_no_match(/INNER JOIN/, nj)
|
||||
end
|
||||
end
|
||||
|
||||
def test_nil_on_disable_joins_through
|
||||
member = members(:blarpy_winkup)
|
||||
assert_nil assert_queries(1) { member.organization }
|
||||
assert_nil assert_queries(1) { member.organization_without_joins }
|
||||
end
|
||||
|
||||
def test_preload_on_disable_joins_through
|
||||
members = Member.preload(:organization, :organization_without_joins).to_a
|
||||
assert_no_queries { members[0].organization }
|
||||
assert_no_queries { members[0].organization_without_joins }
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ class Member < ActiveRecord::Base
|
|||
has_one :sponsor_club, through: :sponsor
|
||||
has_one :member_detail, inverse_of: false
|
||||
has_one :organization, through: :member_detail
|
||||
has_one :organization_without_joins, through: :member_detail, disable_joins: true, source: :organization
|
||||
belongs_to :member_type
|
||||
|
||||
has_many :nested_member_types, through: :member_detail, source: :member_type
|
||||
|
|
|
@ -462,8 +462,8 @@ connections globally.
|
|||
### Handling associations with joins across databases
|
||||
|
||||
As of Rails 7.0+, Active Record has an option for handling associations that would perform
|
||||
a join across multiple databases. If you have a has many through association that you want to
|
||||
disable joining and perform 2 or more queries, pass the `disable_joins: true` option.
|
||||
a join across multiple databases. If you have a has many through or a has one through association
|
||||
that you want to disable joining and perform 2 or more queries, pass the `disable_joins: true` option.
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -471,12 +471,16 @@ For example:
|
|||
class Dog < AnimalsRecord
|
||||
has_many :treats, through: :humans, disable_joins: true
|
||||
has_many :humans
|
||||
|
||||
belongs_to :home
|
||||
has_one :yard, through: :home, disable_joins: true
|
||||
end
|
||||
```
|
||||
|
||||
Previously calling `@dog.treats` without `disable_joins` would raise an error because databases are unable
|
||||
to handle joins across clusters. With the `disable_joins` option, Rails will generate multiple select queries
|
||||
to avoid attempting joining across clusters. For the above association `@dog.treats` would generate the
|
||||
Previously calling `@dog.treats` without `disable_joins` or `@dog.yard` without `disable_joins`
|
||||
would raise an error because databases are unable to handle joins across clusters. With the
|
||||
`disable_joins` option, Rails will generate multiple select queries
|
||||
to avoid attempting joining across clusters. For the above association, `@dog.treats` would generate the
|
||||
following SQL:
|
||||
|
||||
```sql
|
||||
|
@ -484,14 +488,21 @@ SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ? [["dog_id", 1]]
|
|||
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?) [["human_id", 1], ["human_id", 2], ["human_id", 3]]
|
||||
```
|
||||
|
||||
While `@dog.yard` would generate the following SQL:
|
||||
|
||||
```sql
|
||||
SELECT "home"."id" FROM "homes" WHERE "homes"."dog_id" = ? [["dog_id", 1]]
|
||||
SELECT "yards".* FROM "yards" WHERE "yards"."home_id" = ? [["home_id", 1]]
|
||||
```
|
||||
|
||||
There are some important things to be aware of with this option:
|
||||
|
||||
1) There may be performance implications since now two or more queries will be performed (depending
|
||||
on the association) rather than a join. If the select for `humans` returned a high number of IDs
|
||||
the select for `treats` may send too many IDs.
|
||||
2) Since we are no longer performing joins a query with an order or limit is now sorted in-memory since
|
||||
2) Since we are no longer performing joins, a query with an order or limit is now sorted in-memory since
|
||||
order from one table cannot be applied to another table.
|
||||
3) This setting must be added to all associations that you want joining to be disabled.
|
||||
3) This setting must be added to all associations where you want joining to be disabled.
|
||||
Rails can't guess this for you because association loading is lazy, to load `treats` in `@dog.treats`
|
||||
Rails already needs to know what SQL should be generated.
|
||||
|
||||
|
|
Loading…
Reference in a new issue