Using `assign_attributes` saves `has_many through:` association immediately
Asked Answered
C

4

9

As far as I know, assign_attributes (unlike update_attributes) is not supposed to save the record or for that matter, any record.

So it quite startled me when I discovered that this is not true when supplying _ids for a has_many through: relation.

Consider the following example:

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
end

class Group < ApplicationRecord
  has_many :group_users
  has_many :users, through: :group_users
end

class User < ApplicationRecord
  has_many :group_users
  has_many :groups, through: :group_users

  validates :username, presence: true
end

So we have users and groups in an m-to-m relationship.

Group.create # Create group with ID 1
Group.create # Create group with ID 2

u = User.create(username: 'Johny')

# The following line inserts two `GroupUser` join objects, despite the fact 
# that we have called `assign_attributes` instead of `update_attributes` 
# and, equally disturbing, the user object is not even valid as we've 
# supplied an empty `username` attribute.
u.assign_attributes(username: '', group_ids: [1, 26])

The log as requested by a commenter:

irb(main):013:0> u.assign_attributes(username: '', group_ids: [1, 2])
  Group Load (0.2ms)  SELECT "groups".* FROM "groups" WHERE "groups"."id" IN (1, 2)
  Group Load (0.1ms)  SELECT "groups".* FROM "groups" INNER JOIN "group_users" ON "groups"."id" = "group_users"."group_id" WHERE "group_users"."user_id" = ?  [["user_id", 1]]
   (0.0ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["group_id", 1], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.691941"], ["updated_at", "2017-06-29 08:15:11.691941"]]
  SQL (0.1ms)  INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["group_id", 2], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.693984"], ["updated_at", "2017-06-29 08:15:11.693984"]]
   (2.5ms)  commit transaction
=> nil

I daresay that update_attributes and the _ids construct are mostly used for processing web forms - in this case a form that updates the user itself as well as its group association. So I think it is quite safe to say that the general assumption here is all or nothing, and not a partial save.

Am I using it wrong in some way?

Chelseachelsey answered 29/6, 2017 at 7:57 Comment(9)
what makes you say that the record is saved? It is not, try u.valid? and u.save!.Kurth
The fact that I can see the queries and that it is persisted in the database (for the join table, not the record itself of course). And valid? is false and save! raises, as expected, a validation error.Chelseachelsey
You see queries? can you verify doing GroupUser.pluck(:group_id) and u.reload!, u.username?Kurth
The user object is not saved of course, only the association (that's the whole point of this question). And I guarantee you that this is the case...I'm well able to verify that there are join records in the database ;)Chelseachelsey
Please, post the log.Gael
I don't see the point here at all, but I've added the log above. The user is not touched but the join entries are created. I'm very well able to judge whether something is in the DB, trust me on this.Chelseachelsey
Read this: guides.rubyonrails.org/… and github.com/rails/rails/issues/17368Gael
it throws error if group_id is invalidKurth
Thank you @GokulM, this answers my question.Chelseachelsey
M
2

@gokul-m suggests reading about the issue at https://github.com/rails/rails/issues/17368. One of the comments in there points to a temporary workaround: https://gist.github.com/sudoremo/4204e399e547ff7e3afdd0d89a5aaf3e

Morganstein answered 5/6, 2019 at 21:6 Comment(1)
Yes, that's the solution i finally came up with. But I'd still like Rails to fix this - in my oppinion the current behavior is so unexpected that I would regard it as a bug.Chelseachelsey
N
0

an example of my solution to this problem:

  ruby: 
  def assign_parameters(attributes, options = {})
    with_transaction_returning_status {self.assign_attributes(attributes, options)}
  end
Nedi answered 15/11, 2019 at 12:25 Comment(1)
Please put your answer always in context instead of just pasting code. See here for more details.Exoenzyme
R
0

You can handle validation with assign_attributes like so

@item.assign_attributes{ year: "2021", type: "bad" }.valid?
Rope answered 25/2, 2021 at 20:18 Comment(1)
The call to assign_attributes already persisted the changes, so this is not a solution I'm afraid.Chelseachelsey
U
0

Wrapping everything from assigning the values to the save in a transaction worked for me.

User.transaction do
 // assign variables
 // save changes
end
Unchristian answered 21/6, 2024 at 18:54 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.