On my test I'm seeing that after_update is been called after calling FactoryBot.create(:object). Is it normal? As far as I know, it should be called only when a record gets updated, no?
I can see someone reporting this as a bug, with a good explanation here.
To take the essentials from this, if your factory is adding an association (this is an assumption at this stage - if you could add a little more to your question, that'd be great), the code runs as follows:
Example factory
FactoryGirl.create(
:user,
:account => FactoryGirl.create(:account)
)
How this is invoked:
account = Account.new
account.save! # Since this is Ruby, it'll evaluate this line as part of the hash first, before creating the user
user = User.new
user.account = account
user.save! # The hash has been evaluated and we're assigning the account created from the hash
So, if you have an association in there, the account, in this case, would be created, then updated as the association is saved.
To setup your factory to overcome this, you can use the following:
factory :user do
factory :user_with_account do
after_create do |user|
FactoryGirl.create(:account, :user => user)
end
end
end
factory :account do
user
end
How does that apply to your setup? Have a shot and see if it provides a solution - let me know how you get on :)
after_update will only be called when the object is updated, however if your factory has associations or after_create actions, these will often cause the model to be updated, causing after_update to be triggered.
An example, using ActiveRecord 5:
class Client < ApplicationRecord
after_create :ping
after_update :pong
def ping
logger.info("---> after_create HOOK CALLED")
end
def pong
logger.info("---> after_update HOOK CALLED")
end
end
Creating and updating the object act as expected:
c = Client.create!(name: "test")
# (0.4ms) BEGIN
# Client Create (1.4ms) INSERT INTO "clients" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "test"], ["created_at", "2018-05-24 17:06:24.076085"], ["updated_at", "2018-05-24 17:06:24.076085"]]
# ---> after_create HOOK CALLED
# (4.0ms) COMMIT
c.update! name: "test2"
# (0.8ms) BEGIN
# Client Update (2.3ms) UPDATE "clients" SET "name" = $1, "updated_at" = $2 WHERE "clients"."id" = $3 [["name", "test2"], ["updated_at", "2018-05-24 17:06:36.525448"], ["id", "a3d49153-2f25-48c3-8319-61c2fb6ea173"]]
# ---> after_update HOOK CALLED
# (0.9ms) COMMIT
]
And FactoryBot behaves the same:
FactoryBot.create(:client)
# (1.2ms) BEGIN
# Client Create (0.9ms) INSERT INTO "clients" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "Montana tigers"], ["created_at", "2018-05-24 17:11:57.138995"], ["updated_at", "2018-05-24 17:11:57.138995"]]
# ---> after_create HOOK CALLED
# (1.1ms) COMMIT
Related
I have the following relations set up:
user has_many quizzes
quiz belongs_to user
quiz has_many questions
question belongs_to quiz
App is set up to use PostgreSQL. I'm trying to bulk insert a bunch of records using the insert_all! method
begin
quiz = user.quizzes.create!(title: title, slug: slug)
quiz_questions = params[:quiz][:questions].map! do |q|
# creating an attribute hash here (code removed for conciseness of question)
end
result = quiz.questions.insert_all!(quiz_questions)
This threw an error which was caught by my "catch all" block
rescue ActiveRecord::ActiveRecordError
render json: { message: ['Something went wrong'] }, status: 500
The running server console printed this message:
TRANSACTION (0.9ms) BEGIN
↳ app/controllers/quizzes_controller.rb:14:in `create'
Quiz Create (2.8ms) INSERT INTO "quizzes" ("title", "user_id", "slug", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["title", "a quiz"], ["user_id", 1], ["slug", "a-quizk2DqYk"], ["created_at", "2021-12-01 05:00:05.800134"], ["updated_at", "2021-12-01 05:00:05.800134"]]
↳ app/controllers/quizzes_controller.rb:14:in `create'
TRANSACTION (1.6ms) COMMIT
↳ app/controllers/quizzes_controller.rb:14:in `create'
Question Bulk Insert (0.6ms) INSERT INTO "questions" ("question","a","b","c","d","score","answer","quiz_id") VALUES ('what is name', 'str', 'char', 'num', 'bool', 5, 'A', 1), ('die', 'yes', 'no', 'ok', 'what', 5, 'B', 1) RETURNING "id"
↳ (eval):6:in `block in insert_all!'
Completed 500 Internal Server Error in 153ms (Views: 0.2ms | ActiveRecord: 38.1ms | Allocations: 49609)
So I think I am not calling insert_all! correctly because the server just does an insert without the BEGIN and COMMIT bookends. Also, I would like to know which error is being thrown and caught by the catch all block. What would be the correct way to do insert_all! ?
you could wrap your bulk insert into a transaction
def bulk_insert
ActiveRecord::Base.transaction do
quiz = user.quizzes.create!(title: title, slug: slug)
quiz_questions = params[:quiz][:questions].map! do |q|
# creating an attribute hash here
# ...
# note that you could validate attribute manually
raise ActiveRecord::Rollback if q.description.blank?
end
result = quiz.questions.insert_all!(quiz_questions)
end
rescue ActiveRecord::Rollback => e
puts e
end
I seem to run into a some kind of circular relationships that the two solutions in the gem's documentation won't solve for me. See the example below. Is this meant to be done differently?
One would argue that because one object could not really be persisted without the other they ought to just be one model. I think it's better to extract all the logic regarding authentication to it's seperate model in order not to bloat the user. Most of the time credential stuff is only used when creating sessions, whereas the user is used all the time.
create_table "credentials", force: :cascade do |t|
t.bigint "user_id", null: false
...
t.index ["user_id"], name: "index_credentials_on_user_id"
end
add_foreign_key "credentials", "users"
class Credential < ApplicationRecord
belongs_to :user, inverse_of: :credential
end
class User < ApplicationRecord
has_one :credential, inverse_of: :user
validates :credential, presence: true
end
Fabricator(:user_base, class_name: :user)
Fabricator(:user, from: :user_base) do
credential
end
Fabricator(:credential) do
user(fabricator: :user_base)
end
irb(main):001:0> Fabricate(:user)
TRANSACTION (0.1ms) BEGIN
TRANSACTION (0.1ms) ROLLBACK
Traceback (most recent call last):
1: from (irb):1:in `<main>'
ActiveRecord::RecordInvalid (Validation failed: Credential can't be blank)
irb(main):002:0> Fabricate(:credential)
Traceback (most recent call last):
2: from (irb):1:in `<main>'
1: from (irb):2:in `rescue in <main>'
ActiveRecord::RecordInvalid (Validation failed: Credential can't be blank)
irb(main):003:0> Fabricate.build(:user).save
TRANSACTION (0.2ms) BEGIN
User Create (0.8ms) INSERT INTO "users" ("email", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["email", "fake#mail.com"], ["created_at", "2021-05-29 18:19:09.312429"], ["updated_at", "2021-05-29 18:19:09.312429"]]
Credential Create (0.9ms) INSERT INTO "credentials" ("user_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["user_id", 19], ["created_at", "2021-05-29 18:19:09.319411"], ["updated_at", "2021-05-29 18:19:09.319411"]]
TRANSACTION (41.2ms) COMMIT
=> true
The way you're solving this would certainly work. The way I normally recommend people solve this is by overriding the inverse relationships. ActiveRecord will do the right thing in this case.
Fabricator(:user) do
credential { Fabricate.build(:credential, user: nil) }
end
Fabricator(:credential) do
user { Fabricate.build(:user, credential: nil) }
end
The model that has the foreign key, in this case Credential, is the one that is required to have a user_id value to be persisted. This means that there needs to be a user (either in memory or in the database) before creating a credential. This is the reason why using build works for you.
If the user exists in memory, rails will be smart enough to create that one first before creating the credential. It seems to me that when you use build with Fabricate it’s initializing a user and a credential so when the user is saved, it saves the credential with the newly created user.
Note that the docs use this syntax for belongs_to, not has_one. It seems that you may need to refer to the callbacks section of the documentation to fix this issue.
I have two Rails environments. One development environment running Postgres and Rails 5.0.6 and an almost identical environment on Heroku.
I have an Administrator class, which generates a username for an Administrator on a before_save callback based on the user's forename and surname fields.
class Administrator < ApplicationRecord
validates :username, uniqueness: true
validates :forename, presence: true
validates :surname, presence: true
before_save :generate_username
def generate_username
return if username.present?
proposed = "#{forename}#{surname}".downcase
existing_count = Administrator.where("username ILIKE ?", "#{proposed}%").size
self.username = existing_count.zero? ? proposed : "#{proposed}#{existing_count}"
end
end
After the user is validated, a username is generated in the form FORENAMESURNAMEX where X is an incrementing number (or nothing).
Here's the commands I run in the Rails console on my development machine.
irb(main):012:0> Administrator.create(email: 'edward#test.net', forename: 'Edward', surname: 'Scissorhands')
D, [2017-10-13T10:00:18.985765 #280] DEBUG -- : (0.2ms) BEGIN
D, [2017-10-13T10:00:18.987554 #280] DEBUG -- : Administrator Exists (0.5ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."email" = $1 LIMIT $2 [["email", "edward#test.net"], ["LIMIT", 1]]
D, [2017-10-13T10:00:18.988923 #280] DEBUG -- : Administrator Exists (0.4ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."username" IS NULL LIMIT $1 [["LIMIT", 1]]
D, [2017-10-13T10:00:18.990155 #280] DEBUG -- : (0.4ms) SELECT COUNT(*) FROM "administrators" WHERE (username ILIKE 'edwardscissorhands%')
D, [2017-10-13T10:00:18.992000 #280] DEBUG -- : SQL (0.5ms) INSERT INTO "administrators" ("email", "created_at", "updated_at", "username", "forename", "surname") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" [["email", "edward#test.net"], ["created_at", "2017-10-13 10:00:18.990421"], ["updated_at", "2017-10-13 10:00:18.990421"], ["username", "edwardscissorhands"], ["forename", "Edward"], ["surname", "Scissorhands"]]
D, [2017-10-13T10:00:18.995845 #280] DEBUG -- : (1.8ms) COMMIT
=> #<Administrator id: 10, email: "edward#test.net", created_at: "2017-10-13 10:00:18", updated_at: "2017-10-13 10:00:18", role: nil, otp_public_key: nil, username: "edwardscissorhands", forename: "Edward", surname: "Scissorhands">
As you can see, the callback is executed and the user's username is generated and persisted to the database as expected.
However, when I run the same code on a our test environment running on Heroku (and Heroku Postgres), this is what happens:
irb(main):005:0> Administrator.create!(email: 'edward#test.net', forename: 'Edward', surname: 'Scissorhands')
(1.9ms) BEGIN
Administrator Exists (1.1ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."email" = $1 LIMIT $2 [["email", "edward#test.net"], ["LIMIT", 1]]
Administrator Exists (0.9ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."username" IS NULL LIMIT $1 [["LIMIT", 1]]
(0.9ms) ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: Username has already been taken
(I'm using create! here instead of create to show the validation errors that do not occur in development.)
I don't see why the behaviour should differ between environments. Both are running identical versions of Rails (5.0.6) and are running identical codebases.
before_save is called after validation, hence the error.
Try before_validation instead.
For reference here's the order callbacks are called when creating an object:
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback
The logic in your code is flawed. This is a legitimate bug; you need to redesign how username generation words.
For example, suppose there is one user in your system called: edwardscissorhands1. There is no edwardscissorhands, and no edwardscissorhands2/3/4 etc.
The line: Administrator.where("username ILIKE ?", "edwardscissorhands%").size returns 1, and then your logic tries to create a new user that already exists.
... I cannot say for sure what has happened on your production server without seeing the actual data, but I bet it's something like this. It could be slightly more convoluted, e.g. the users: tom, tom3 and tomlord exist; therefore your logic tries to create a second tom3 user.
For example, this might have happened if you generated some edwardscissorhards users, then deleted one or more of them.
As an example, here's one way you could redesign the logic:
def generate_username
return if username.present?
proposed = "#{forename}#{surname}".downcase
return proposed unless Administrator.exists?("username ILIKE ?", proposed)
counter = 1
while(Administrator.exists?("username ILIKE ?", "#{proposed}#{counter}"))
counter += 1
end
"#{proposed}#{counter}"
end
This could probably be improved performance-wise, although the multiple database queries here are unlikely to be a major issue in the real application (assuming you don't get lots of administrators with the same name!).
I'm trying to create a Rails plugin. For the most part, what I've written works. However, there's a problem with associations. When I try to call an association, I get this error:
ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
At the moment, the plugin looks like this:
module ControlledVersioning
module ActsAsVersionable
extend ActiveSupport::Concern
included do
has_many :versions, as: :versionable
after_create :create_initial_version
end
module ClassMethods
def acts_as_versionable(options = {})
cattr_accessor :versionable_attributes
self.versionable_attributes = options[:versionable_attributes]
end
end
private
def create_initial_version
version = versions.create
end
end
end
ActiveRecord::Base.send :include, ControlledVersioning::ActsAsVersionable
Again, the error message is triggered whenever I try to call the association. I used debugger in the after_create callback and tried running:
> versions.create
*** ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
> versions
*** ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
> Version.new
#<Version id: nil, versionable_id: nil, versionable_type: nil>
There are a few things you need to change in your code in order for it to work.
First, versions is a reserved keyboard from rails -- you can't have a relationship with that name - (I used the name versionings in order to make it work)
Also, you want to make sure to just add has_many versionings for the models that want to acts_as_versionable - meaning, move has_many :versionings, as: :versionable, class_name: 'Version' and after_create :create_initial_version calls to inside the acts_as_versionable method.
Here's how all together will look like:
module ControlledVersioning
module ActsAsVersionable
extend ActiveSupport::Concern
module ClassMethods
def acts_as_versionable(options = {})
has_many :versionings, as: :versionable, class_name: 'Version'
after_create :create_initial_version
cattr_accessor :versionable_attributes
self.versionable_attributes = options[:versionable_attributes]
end
end
private
def create_initial_version
version = versionings.create
end
end
end
ActiveRecord::Base.send :include, ControlledVersioning::ActsAsVersionable
Doing those changes made the plugin work for me:
irb(main):003:0> Post.create!
(0.1ms) begin transaction
Post Create (0.7ms) INSERT INTO "posts" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2019-07-16 08:55:13.768196"], ["updated_at", "2019-07-16 08:55:13.768196"]]
Version Create (0.2ms) INSERT INTO "versions" ("versionable_type", "versionable_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["versionable_type", "Post"], ["versionable_id", 3], ["created_at", "2019-07-16 08:55:13.772246"], ["updated_at", "2019-07-16 08:55:13.772246"]]
(2.0ms) commit transaction
=> #<Post id: 3, created_at: "2019-07-16 08:55:13", updated_at: "2019-07-16 08:55:13", name: nil>
irb(main):004:0> Post.last.versionings
Post Load (0.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT ? [["LIMIT", 1]]
Version Load (0.2ms) SELECT "versions".* FROM "versions" WHERE "versions"."versionable_id" = ? AND "versions"."versionable_type" = ? LIMIT ? [["versionable_id", 3], ["versionable_type", "Post"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Version id: 2, versionable_type: "Post", versionable_id: 3, created_at: "2019-07-16 08:55:13", updated_at: "2019-07-16 08:55:13">]>
irb(main):005:0>
I would try extending active record in an initializer instead of including it.
initializers/acts_as_versionable.rb
ActiveRecord::Base.extend(ControlledVersioning::ActsAsVersionable)
Also in development; or any environment that reloads the files you'll likely see an error like has been removed from the module tree but is still active. Make sure you're plugin file is in config.eager_load_paths and not actually in a concern path.
I'm trying to push values onto a serialized text field (acting as array).
In the controller I have
class DeliveriesController < ApplicationController
def new
#delivery = Delivery.new
end
def create
#user = current_user
#user.deliveries.create(params[:delivery])
#user.recent_addresses.shift if #user.recent_addresses.size >= 10
#user.recent_addresses.push(params[:delivery][:from_address])
#user.save
redirect_to root_path
end
end
User model
serialize :recent_addresses, Array
attr_accessible :recent_addresses
has_many :deliveries
The problem is that the user is not being saved with the new recent addresses. The from_address is being added within the controller but when I try save it rollsback and the recent addresses array is empty.
Parameters: {"delivery"=>{"from_address"=>"xyz"}, "commit"=>"Submit"}
SQL (0.6ms) INSERT INTO "deliveries" ("created_at", "from_address", "user_id") VALUES ($1, $2) RETURNING "id" [["created_at", Fri, 25 Oct 2013 13:21:50 UTC +00:00], ["from_address", "xyz"], ["user_id", 1]]
(0.8ms) COMMIT
(0.1ms) BEGIN
(0.2ms) ROLLBACK
Redirected to http://localhost:3000/