ActiveRecord has_many through a scoped association with arg - ruby-on-rails

I've seen a number of answers to questions that address how to use scope blocks in ActiveRecord associations that include passing the object itself into the block like ...
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { baz: patron.blah },
foreign_key: :somekey,
primary_key: :somekey
end
class Bar < ActiveRecord::Base
belongs_to :patron,
->(bar) { blah: bar.baz },
foreign_key: :somekey,
primary_key: :somekey
end
The usage of the explicit PK and FK here is due to the legacy relationship between the underlying tables. There are many hundreds of millions of "patrons" in the production system.
As a point of clarification re #TJR - the relationship between Patron and Bar is actual a compound foreign key on the fields :somekey and :baz (or :blah in the reverse direction). ActiveRecord's :foreign_key option doesn't allow arrays.
I've discovered that unfortunately this prevents subsequent has_many :throughs from working as expected.
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { baz: patron.blah },
foreign_key: :somekey,
primary_key: :somekey
has_many :drinks, through: :bars
end
Using the through association produces errors like ...
ArgumentError:
wrong number of arguments (0 for 1)
The association between bar and drinks is a classic has_many with the standard foreign and primary key (bar_id, id).
I have come up with some ugly work arounds that allow me to accomplish the desired functionality but the whole thing smells terrible. So far the best of these look like
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { baz: patron.blah },
foreign_key: :somekey,
primary_key: :somekey
def drinks
bars.drinks
end
end
I've received the existing Bar class and it consists of many hundreds of millions of records, as I previously mentioned, making a database side change difficult.
Some posts seemed to suggest a dynamic string evaluation inside the proc to avoid having to pass in the current patron object - but as mentioned in other posts, this doesn't work.
Please advise me on what I might do to get the has_many through relationship working.

I just have tried this kind of associations in Rails 4.2. It works pretty well:
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { where(baz: patron.blah) },
foreign_key: :patron_id,
primary_key: :id
has_many :drinks, through: :bars
end
class Bar < ActiveRecord::Base
belongs_to :patron,
->(bar) { where(blah: bar.baz) },
foreign_key: :patron_id,
primary_key: :id
has_many :drinks
end
class Drink < ActiveRecord::Base
end
Check associations:
> p1 = Patron.first
> p1.drinks
Drink Load (0.8ms) SELECT "drinks".* FROM "drinks" INNER JOIN "bars" ON "drinks"."bar_id" = "bars"."id" WHERE "bars"."patron_id" = 1 AND "bars"."baz" = 1 [["patron_id", 1], ["baz", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Drink id: 3, name: "drink 3", bar_id: 2, created_at: "2017-04-07 03:30:06", updated_at: "2017-04-07 03:30:06">]>

Related

ActiveRecord has_one where associated model has two belongs_to associations

I have two ActiveRecord models that are associated with each other in this way:
class Address < ApplicationRecord
has_one :user, class_name: User.name
end
class User < ApplicationRecord
belongs_to :home_address, class_name: Address.name
belongs_to :work_address, class_name: Address.name
end
The User -> Address association works fine:
home_address = Address.new
#=> <Address id:1>
work_address = Address.new
#=> <Address id:2>
user = User.create!(home_address: home_address, work_address: work_address)
#=> <User id:1, home_address_id: 1, work_address_id: 2>
user.home_address
#=> <Address id:1>
user.work_address
#=> <Address id:2>
What I'm having trouble with is getting the Address's has_one to work properly. At first I got an error that User#address_id does not exist, which makes sense because that's not the name of the foreign key field. It would be either home_address_id or work_address_id (and I added these FKs with a migration). But I wasn't sure how to let it know which address to use, until I learned that you can pass a scope into a has_one declaration:
class Address < ApplicationRecord
has_one :user,
->(address) { where(home_address_id: address.id).or(where(work_address_id: address.id)) },
class_name: User.name
end
But this returns the same error as before: Caused by PG::UndefinedColumn: ERROR: column users.address_id does not exist. This is confusing, because nowhere in that scope did I declare that I'm looking on address_id. I'm guessing the has_one implicitly has a foreign_key of :address_id, but I don't know how I'd set this because there are technically two, :home_address_id and :work_address_id.
I feel like I'm close here - how do I fix this has_one association?
Update
My gut says that the solution here is to just create a user method that performs the query I'm looking to run, instead of declaring a has_one. It'd be great if has_one supports this functionality, but if not, I'll fall back to that.
class Address < ApplicationRecord
def user
User.find_by("home_address_id = ? OR work_address_id = ?", id, id)
end
end
Solution
Thanks to #max below! I ended up going with a solution based on his answer. I also use the Enumerize gem, which will come into play in the Address model.
class AddAddressTypeToAddresses < ActiveRecord::Migration[5.2]
add_column :addresses, :address_type, :string
end
class User < ApplicationRecord
has_many :addresses, class_name: Address.name, dependent: :destroy
has_one :home_address, -> { Address.home.order(created_at: :desc) }, class_name: Address.name
has_one :work_address, -> { Address.work.order(created_at: :desc) }, class_name: Address.name
end
class Address < ApplicationRecord
extend Enumerize
TYPE_HOME = 'home'
TYPE_WORK = 'work'
TYPES = [TYPE_HOME, TYPE_WORK]
enumerize :address_type, in: TYPES, scope: :shallow
# Shallow scope allows us to call Address.home or Address.work
validates_uniqueness_of :address_type, scope: :user_id, if: -> { address_type == TYPE_WORK }
# I only want work address to be unique per user - it's ok if they enter multiple home addresses, we'll just retrieve the latest one. Unique to my use case.
end
Each association in Rails can just have a single foreign key because what you would need is in terms of SQL is:
JOINS users
ON users.home_address_id = addresses.id OR users.work_address_id = addresses.id
Using a lambda to add a default scope for the association won't work here since ActiveRecord doesn't actually let you monkey with how it joins on an assocation level. Which is quite understandable if you consider how many different queries it generates and the number of edge cases that feature would cause.
If you REALLY want to go down the rabbit hole of having two different foreign keys on your users table you can solve it with Single Table Inheritance:
class AddTypeToAddresses < ActiveRecord::Migration[6.1]
def change
add_column :addresses, :type, :string
end
end
class User < ApplicationRecord
belongs_to :home_address, class_name: 'HomeAddress'
belongs_to :work_address, class_name: 'WorkAddress'
end
class HomeAddress < Address
has_one :user, foreign_key: :home_address_id
end
class WorkAddress < Address
has_one :user, foreign_key: :work_address_id
end
But I would put the foreign key on the other table and use a one-to-many association:
class Address < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :addresses
end
This lets you add as many address types as you want without borking your users table.
If you want to limit the user to one home and one work address you would do:
class AddTypeToAddresses < ActiveRecord::Migration[6.1]
def change
add_column :addresses, :address_type, :integer, index: true, default: 0
add_index :addresses, [:user_id, :address_type], unique: true
end
end
class Address < ApplicationRecord
belongs_to :user
enum address_type: {
home: 0,
work: 1
}
validates_uniqueness_of :type, scope: :user_id
end
class User < ApplicationRecord
has_many :addresses
has_one :home_address,
-> { home },
class_name: 'Address'
has_one :work_address,
-> { work },
class_name: 'Address'
end

Rails 4 has_many though where filtering

I'm trying to figure out if it's possible to group/filter/select by a records' attribute though a join table. While I understand there are other ways of achieving the same result (one of which I use below) I was wondering if the same could be achieved with a HMT relation.
The models are as follows:
class Foo < ActiveRecord::Base
has_many :foo_bars
has_many :bars, through: foo_bars
# SOMETHING LIKE THIS:
has_many :group_a_bars, -> where { foo_bars.bar.bar_group_id: 1 }
end
class Bar < ActiveRecord::Base
# == Schema Information
#
# Table name: bars
# bar_group_id :integer not null
belongs_to :bar_group
end
class FooBar < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
class BarGroup < ActiveRecord::Base
has_many :bars
end
What I would like to be able to do in the view is something like: #{foo.group_a_bars} which would all of the FooBars that have bars that belong to the first bar_group
The same could be accomplished with something like:
foo.foo_bars.find_all{ |foo_bar| foo_bar.bar.bar_group_id == 1 }
EDIT: Following the advice of #smathy I added a join and got it working with the following:
class Foo < ActiveRecord::Base
has_many :foo_bars
has_many :bars, through: foo_bars
# WORKING:
has_many :group_a_bars,
-> { joins(:bar).where(bars: { bar_group_id: 1 }) },
class_name: "FooBar",
source: :bar
end
Yes, it'll be something like this:
has_many :group_a_bars,
-> { joins(:bar).where( bar: { bar_group_id: 1 } ) }, through: :foo_bars, class_name: "Bar"
You might need a :source option in there too.

How to set a 'has many :through' a polymorphic association

I have defined my models as follows. I am trying to do #user.orders using has_many. I have defined a method:orders to show the behaviour I want.
class Location < ActiveRecord::Base
belongs_to :locationable, polymorphic: true
has_many :orders, ->(location) { where(location.locationable_type == 'User') }, class_name: 'Order', foreign_key: :drop_location_id
# but this does not check for type
# it produces the SQL: SELECT "orders".* FROM "orders" WHERE "orders"."drop_location_id" = $1
end
class Order < ActiveRecord::Base
# location is polymorphic, so type of location can be merchant/user
belongs_to :pickup_location, class_name: 'Location'
belongs_to :drop_location, class_name: 'Location'
end
class User < ActiveRecord::Base
has_many :locations, as: :locationable
# TODO: make it a has_many :orders instead
def orders
Order.where(drop_location: locations)
end
end
Using a method doesn't feel like the rails way. Moreover, I want it to work well with rails_admin.
By now, you should have received an error indicating that you can't use has_many through a polymorphic association. If you think about it, it makes perfect sense, how could your ORM (ActiveRecord) formulate the query as the joins would be horrendous.

Create new parent record with has_many :through relationship for existing children

I am working on a Ruby on Rails API (version 4.0) to create and update invoices. The relationship between invoices and products is a has_many trough: relationship. Imagine I have product 1, 2, & 3. I am having trouble creating a new invoice that contains product 1 & 3.. When I run the code below I get the error:
Unknown primary key for table invoices_products in model InvoicesProduct.
This error doesn't really make sense to me since InvoicesProduct is a join table and shouldn't require a primary key.
One tricky part about the design is that it needs to track which employee added which products to the invoice, which is why invoices_product has employee_id. It does not seem to be the cause of the problem at the moment. Here is the DB design of the tables in questions:
InvoicesController
This is the code I currently have in the controller. The error message occurs on the first line of create:
def create
invoice = Invoice.new(create_invoice_params)
invoice.created_by = #current_user
# eventually need to set employee_id on each invoice_product,
# but just need to get it working first
# invoice.invoices_products.map!{|link| link.employee = #current_user }
invoice.save
respond_with invoice
end
def create_invoice_params
params.fetch(:invoice).permit(:customer_id, :status_code, :payment_method_code, product_ids: [])
end
Invoice
# /app/models/invoice.rb
class Invoice < ActiveRecord::Base
validates_presence_of :customer
validates_presence_of :created_by
belongs_to :customer, inverse_of: :invoices
belongs_to :created_by, inverse_of: :invoices, class_name: 'Employee'
has_many :invoices_products, inverse_of: :invoice
has_many :products, through: :invoices_products
end
InvoicesProduct
class InvoicesProduct < ActiveRecord::Base
validates_presence_of :invoice
validates_presence_of :product
validates_presence_of :employee
belongs_to :product, inverse_of: :invoices_products
belongs_to :invoice, inverse_of: :invoices_products
belongs_to :employee, inverse_of: :invoices_products
end
Product
# /app/models/product.rb
class Product < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 100 }
has_many :invoices_products, inverse_of: :product
has_many :invoices, through: :invoices_products
end
Request
This is what I've got in mind for a working request, the solution doesn't need to match this, but its what I've been trying
{
"invoice": {
"customer_id": "1",
"product_ids": ["1", "5", "8"]
}
}
I was able to fix the relationship by adding a primary key to the invoices_products. For some reason I was under the impression that join tables did not require a primary key for has_many :through relationships. However, looking at the example on the Rails guide site, the example join table does have a primary key.
That is because you are using has_many :through. If you don't want id (or any other additional field) in the join table, use has_many_and_belongs_to instead

"has_many :through" association through a polymorphic association with STI

I have two models that use the people table: Person and Person::Employee (which inherits from Person). The people table has a type column.
There is another model, Group, which has a polymorphic association called :owner. The groups table has both an owner_id column and an owner_type column.
app/models/person.rb:
class Person < ActiveRecord::Base
has_one :group, as: :owner
end
app/models/person/employee.rb:
class Person::Employee < Person
end
app/models/group.rb:
class Group < ActiveRecord::Base
belongs_to :owner, polymorphic: true
belongs_to :supervisor
end
The problem is that when I create a Person::Employee with the following code, the owner_type column is set to an incorrect value:
group = Group.create
=> #<Group id: 1, owner_id: nil, owner_type: nil ... >
group.update owner: Person::Employee.create
=> true
group
=> #<Group id: 1, owner_id: 1, owner_type: "Person" ... >
owner_type should be set to "Person::Employee", but instead it is set to "Person".
Strangely, this doesn't seem to cause any problems when calling Group#owner, but it does cause issues when creating an association like the following:
app/models/supervisor.rb:
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :employees, through: :groups, source: :owner,
source_type: 'Person::Employee'
end
With this type of association, calling Supervisor#employees will yield no results because it is querying for WHERE "groups"."owner_type" = 'People::Employees' but owner_type is set to 'People'.
Why is this field getting set incorrectly and what can be done about it?
Edit:
According to this, the owner_type field is not getting set incorrectly, but it is working as designed and setting the field to the name of the base STI model.
The problem appears to be that the has_many :through association searches for Groups with a owner_type set to the model's own name, instead of the base model's name.
What is the best way to set up a has_many :employees, through: :group association that correctly queries for Person::Employee entries?
You're using Rails 4, so you can set a scope on your association. Your Supervisor class could look like:
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :employees, lambda {
where(type: 'Person::Employee')
}, through: :groups, source: :owner, source_type: 'Person'
end
Then you can ask for a supervisor's employees like supervisor.employees, which generates a query like:
SELECT "people".* FROM "people" INNER JOIN "groups" ON "people"."id" =
"groups"."owner_id" WHERE "people"."type" = 'Person::Employee' AND
"groups"."supervisor_id" = ? AND "groups"."owner_type" = 'Person'
[["supervisor_id", 1]]
This lets you use the standard association helpers (e.g., build) and is slightly more straightforward than your edit 2.
I did came up with this workaround, which adds a callback to set the correct value for owner_type:
class Group < ActiveRecord::Base
belongs_to :owner, polymorphic: true
before_validation :copy_owner_type
private
def copy_owner_type
self.owner_type = owner.type if owner
end
end
But, I don't know if this is the best and/or most elegant solution.
Edit:
After finding out that the owner_type field is supposed to be set to the base STI model, I came up with the following method to query for Person::Employee entries through the Group model:
class Supervisor < ActiveRecord::Base
has_many :groups
def employees
Person::Employee.joins(:groups).where(
'people.type' => 'Person::Employee',
'groups.supervisor_id' => id
)
end
end
However, this does not seem to cache its results.
Edit 2:
I came up with an even more elegant solution involving setting the has_many :through association to associate with the base model and then creating a scope to query only the inherited model.
class Person < ActiveRecord::Base
scope :employees, -> { where type: 'Person::Employee' }
end
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :people, through: :groups, source: :owner, source_type: 'Person'
end
With this I can call Supervisor.take.people.employees.

Resources