Richbits
Richbits

Reputation: 7624

Rails association attributes access from model

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_attributesand 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

Answers (3)

Arctodus
Arctodus

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

muZk
muZk

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

Pablo
Pablo

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

Related Questions