Reputation: 7624
I have an rails 5 API only rails app with a many to many relationship as follows:
class Business < ApplicationRecord
has_many :managements, inverse_of: :business, autosave: true
has_many :managers, through: :managements, source: :user, autosave: true
end
class Management < ApplicationRecord
belongs_to :user
belongs_to :business
validates :user, uniqueness: { scope: :business }
end
class User < ApplicationRecord
has_many :managements
has_many :businesses, through: :managements
end
within the Business class I have the following method which I want to use to set the owner on the Management join table.
def owner=(user)
userAsManager = managements.find_by(user_id: user.id)
unless userAsManager
managers << user
self.save
userAsManager = managements.find_by(user_id: user.id)
end
if userAsManager && !userAsManager.owner
if owner
self.managements_attributes = [
{ id: managements.find_by(user_id: owner.id).id, owner: false }
]
save
end
self.managements_attributes = [ {id: userAsManager.id, owner: true}]
save
end
end
The reason for this logic in a setter is to manage all situations where there are no managers, where there is an existing owner etc. What I have found is that in an Rspec test such as:
it 'added when the owner is already a manager' do
business.managers << owner
business.save
business.owner = owner
business.save
expect(Business.first.owner).to eq owner
expect(Business.first.managers[0]).to eq owner
end
I have to keep saving the model otherwise when I access self within the owner=(user)
method the associated models are not available. e.g. within the test having set owner
as a business.managers
if the business
is not saved, then on setting the owner to be an owner of the business business.owner = owner
when entering the owner=(user)
method self.managers
is an empty object.
Why is this and is there a way I can do updates within the model and then save it once complete within the test?
TBH I am not that happy with a lot of this code, I don't really understand why I need self on self.managements_attributes
and why I can't just use managements.find_by(user_id: user.id).owner = true
to set the owner, so if you can point me to some good resources that can explain association use at a deeper lever, that would really help.
Upvotes: 0
Views: 195
Reputation: 5847
To access in memory objects that havent been saved yet a couple of changes are needed. First, use inverse_of
on both sides to link the associations.
Example:
class Business < ApplicationRecord
has_many :managements, inverse_of: :business
end
class Management < ApplicationRecord
belongs_to :business, inverse_of: :managements
end
To look up records that are added to the association but not yet saved to the DB you cant use find_by
because that method calls the database. Instead you can use Array#find
. Something like this:
def owner=(user)
management = managements.find { |m| m.user == user } || self.managments.new(user: user)
management.owner ||= management.owner.blank?
end
Upvotes: 1
Reputation: 3038
There are a couple of things...
First, about business.save on tests:
From Rails guide, on "Controlling Caching":
All of the association methods are built around caching, which keeps the result of the most recent query available for further operations.
So you have to options:
a) in tests intead of business.save
, you could use business.reload
or
b) in your owner=
method, reload your association before using find_by: managements.reload
Here is abit of information regarding read in tests: In-Memory vs. Database Layer -- .reload
Second, About self.managements_attributes
why I can't just use managements.find_by(user_id: user.id).owner = true to set the owner
Because you are not updating the owner in database with that call. Your call is like this:
to_update = managements.find_by(user_id: user.id)
to_update.owner = true
That changes is not persised in database. To update db you can do this:
user_as_manager = managements.find_by(user_id: user.id)
user_as_manager.owner = true
user_as_manager.save # this will persist the owner to the database
Which is the same as
user_as_manager = managements.find_by(user_id: user.id)
user_as_manager.update(owner: true)
Which is the same as
managements.find_by(user_id: user.id).update(owner: true)
Upvotes: 1
Reputation: 3005
I think the code could be like this. You wrote something about your code not working as it should (so you used attributes). Does this code have the same problem? I added many reloads because of the problem you mentioned in your tests. Maybe they are not needed.
def owner=(user)
#Added reload. Could this solve the problem about empty managements in tests?
userAsManager = managements.reload.find_by(user_id: user.id)
unless userAsManager
managers << user
userAsManager = managements.reload.find_by(user_id: user.id)
end
if userAsManager
#Just for simplicity, remove all other owners and add this one.
managements.update_all(owner: false)
userAsManager.update_attributes(owner: true)
managements.reload
end
end
Upvotes: 1