I've got some STI in my data model. There are two types of Task records: PrimaryTask and SecondaryTask. So my ActiveRecord models look like this:
class Task < ActiveRecord::Base
end
class PrimaryTask < Task
has_many :secondary_tasks
end
class SecondaryTask < Task
belongs_to :primary_task
end
I want to provide a way to "promote" a SecondaryTask to a PrimaryTask permanently (as in, persisted in the database). From perusing the docs, looks like the #becomes! method is what I want, but I can't get it to save the changes in the database.
id = 1
secondary_task = SecondaryTask.find(id)
primary_task = secondary_task.becomes!(PrimaryTask)
primary_task.id # => 1
primary_task.class # => PrimaryTask
primary_task.type # => "PrimaryTask"
primary_task.new_record? # => false
primary_task.changes # => { "type"=>[nil,"PrimaryTask"] }
primary_task.save! # => true
primary_task.reload # => raises ActiveRecord::RecordNotFound: Couldn't find PrimaryTask with id=1 [WHERE "tasks"."type" IN ('PrimaryTask')]
# Note: secondary_task.reload works fine, because the task's type did not change in the DB
Any idea what's up? I tried the following things, to no avail. Am I misunderstanding becomes!?
Force the record to be 'dirty' in case the save! call was a no-op because none of the attributes were marked dirty (primary_task.update_attributes(updated_at: Time.current) -- didn't help)
Destroy secondary_task in case the fact that they both have the same id was a problem. Didn't help. The SecondaryTask record was deleted but no PrimaryTask was created (despite the call to save! returning true)
UPDATE 1
The logs show the probable issue:
UPDATE "tasks" SET "type" = $1 WHERE "tasks"."type" IN ('PrimaryTask') AND "tasks"."id" = 2 [["type", "PrimaryTask"]]
So the update is failing because the WHERE clause causes the record not to be found.
Figured it out. Turns out there was a bug in ActiveRecord version 4.0.0. It has since been patched. The key change this patch introduced was to set the changes correctly in both instances. So now you can call save on the original instance (in my example secondary_task) and it will change the type in the database. Note that calling save on the new instance (for me primary_task) will NOT save the changes, because of the behavior described in the question: it will include a WHERE clause in the SQL UPDATE call that will cause the record not to be found and thus the call to do nothing.
Here's what works with ActiveRecord > 4.1.0:
id = 1
secondary_task = SecondaryTask.find(id)
primary_task = secondary_task.becomes!(PrimaryTask)
secondary_task.changes # => { "type"=>["SecondaryTask","PrimaryTask"] }
primary_task.changes # => { "type"=>["SecondaryTask","PrimaryTask"] }
secondary_task.save! # => true
primary_task.reload # => works because the record was updated as expected
secondary_task.reload # => raises ActiveRecord::RecordNotFound, as expected
Related
I have the following model
class Job < ActiveRecord::Base
attr_accessor :incentive
end
I want to be able to store a temporary column in my model via attr_accessor.
I want to be able to do something like this
job = Job.last
job.incentive = {id: 1}
and i expect if i do job.incentive, it should return {id: 1}
I also tried doing this as well
def incentive =(val)
#incentive = val
end
def incentive
#incentive
end
But that also didn't work. How can i be able to store temporary column values in rails 4
You script is fine, you'll find the below script working perfectly in your rails console:
job = Job.last
job.incentive = { id: 1 }
p job.incentive # returns { id: 1 }
If you restart or refresh your console (or webpage) this information is gone, since it is only set in memory and not stored to the database.
I'm using activerecord-import gem to import multiple ActiveRecord documents in single query. Then I'm initializing related associations using saved documents ids and importing them too, and so on.
However, I need to have PaperTrail gem versions with create event for all saved documents.
Is there some straightforward way to initialize them to make possible perform bulk insert then?
NOTE: AR-Import gem ignores all callbacks, so I have manually handle them after importing.
Thanks!
UPD-20/05/17:
At the moment I've solved this with the patch for PaperTrail::Model. Here is my .../initializers/paper_trail.rb:
module PaperTrail
class Version < ActiveRecord::Base
...
end
module ModelPatch
extend ActiveSupport::Concern
included do
# new method added to PaperTrail::Model module to make possible initialize
# `create` versions right after importing resources.
# This method contains modified code from private PaperTrail::Model::InstanceMethods#record_create
# Difference:
# - instead of `create!` we use `new` but with validation and raise exception if it's invalid.
# This is for reinsurance that if something changes after update PaperTrail gem in future
# everything still works or need to fix it.
def initialize_record_create_version
return nil unless paper_trail_switched_on?
data = {
event: paper_trail_event || 'create',
whodunnit: PaperTrail.whodunnit
}
if changed_notably? && self.class.paper_trail_version_class.column_names.include?('object_changes')
data[:object_changes] = if self.class.paper_trail_version_class.object_changes_col_is_json?
changes_for_paper_trail
else
PaperTrail.serializer.dump(changes_for_paper_trail)
end
end
new_v = send(self.class.versions_association_name).new merge_metadata(data)
new_v.valid? ? new_v : fail("Invalid PaperTrail Version: #{new_v.errors&.messages}")
end
end
end
end
PaperTrail::Model.send(:include, PaperTrail::ModelPatch)
UPD-28/01/21:
with PaperTrail v10.x.x works this:
def initialize_record_create_version
return nil unless paper_trail.enabled?
event = Events::Create.new(self, false)
data = event.data.merge!(paper_trail.data_for_create)
new_v = send(self.class.versions_association_name).new data
new_v.valid? ? new_v : fail("Invalid PaperTrail Version: #{new_v.errors&.messages}")
end
Yes, it should be possible to bulk-insert records into your versions table.
insert into versions
(item_id, item_type, event, created_at)
(1, "User", "create", "2016-02-29"),
(2, "User", "create", "2016-02-29"),
(1, "Widget", "create", "2016-02-29")
Because it is possible with plain SQL, it is also possible with ActiveRecord. Remember, PaperTrail::Version is a regular AR model.
I have two models with the [fields]:
Order [:date]
Delivery Slot [:day]
Order belongs_to :delivery_slot
When an order is created, I want a delivery slot to be created with the :day set to the order :date.
So far I have created a new method create_delivery_slots in the Order controller that creates a Delivery Slot when the Order is created, but where I am stumped is, how do I get the Order :date in the Delivery Slot :day field?
#Create delivery slots if they dont already exist
def create_delivery_slots
existingslots = []
existingslots = DeliverySlot.all.select {|slot| slot.day == #order.date}
if existingslots.empty?
slot = DeliverySlot.new(:day => #order.date)
slot.save!
end
I have tried multiple approaches, but no luck. My gut tells me its something to do with strong parameters but I can't figure it out...
I'm not sure exactly of how you're set up but you'll probably want something like this:
class Order < ActiveRecord::Base
has_a :delivery_slot
after_create => :create_delivery_slots
.
#other code stuffs
.
.
private
def create_delivery_slots
existingslots = []
existingslots = DeliverySlot.all.select {|slot| slot.day == self.date}
if existingslots.empty?
slot = DeliverySlot.new(:day => self.date)
slot.save!
end
end
end
That's untested but it should be basically what you need.
I am trying update my DB after doing some create
I have a two models
one is ratings model and another is names model what I am doing is
in ratings model
after_create :update_post_update_attribscore
def update_post_update_attribscore
self.names.update_attribscore
end
And In my companies model
def update_attribscore
rating_size1 = ratings.collect(&:r1).count
update_attributes(:attrib1 => company_ratings.collect(&:r1).sum.to_f/rating_size1)
rating_size2 = ratings.collect(&:r2).count
update_attributes(:attrib2 => company_ratings.collect(&:r2).sum.to_f/rating_size2)
review = rating_size1 + rating_size2
update_attributes(:reviews => review)
end
after_update :update_satisfaction
def update_satisfaction
update_attributes(:satisfaction => (attrib1 + attrib2) / self.reviews
end
But now I am getting the following error: nil can't be coerced into Fixnum
So here what I wanted to do is after the update of update_attribscore and I would like to update the update_satisfaction for this where I get data from the above update.
So what is my problem and how can I do it. Can any tell me how to do it.
You are updating the companies model (in update_attribscore) in three steps. update_satisfaction is triggered in every step. You don't say where exactly you're getting the error, but I am guessing that is after the first update (for attrb1), when attrib2 is stil nil.
Do you have any reason for not updating all attributes at once? That way the after_update will only be triggered after all fields are populated. It will be more efficient as well because you'll only hit the database once. That is:
rating_size1 = ratings.collect(&:r1).count
rating_size2 = ratings.collect(&:r2).count
review = rating_size1 + rating_size2
attrb1 = ..../rating_size1
attrb2 = ..../rating_size1
update_attributes :attrb1 => attrb1, :attrb2 => attrb2, :review => review
I have a column named updated_at in postgres. I'm trying to have the db set the time by default. But Rails still executes the query updated_at=NULL. But postgres will only set the timestamp by default when updated_at is not in the query at all.
How do I have Rails exclude a column?
You can disable this behaviour by setting ActiveRecord::Base class variable
record_timestamps to false.
In config/environment.rb, Rails::Initializer.run block :
config.active_record.record_timestamps = false
(if this doesn't work, try instead ActiveRecord::Base.record_timestamps = false at the end of the file)
If you want to set only for a given model :
class Foo < ActiveRecord::Base
self.record_timestamps = false
end
Credit to Jean-François at http://www.ruby-forum.com/topic/72569
I've been running into a similar issue in Rails 2.2.2. As of this version there is an attr_readonly method in ActiveRecord but create doesn't respect it, only update. I don't know if this has been changed in the latest version. I overrode the create method to force is to respect this setting.
def create
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
end
quoted_attributes = attributes_with_quotes(true, false)
statement = if quoted_attributes.empty?
connection.empty_insert_statement(self.class.table_name)
else
"INSERT INTO #{self.class.quoted_table_name} " +
"(#{quoted_attributes.keys.join(', ')}) " +
"VALUES(#{quoted_attributes.values.join(', ')})"
end
self.id = connection.insert(statement, "#{self.class.name} Create",
self.class.primary_key, self.id, self.class.sequence_name)
#new_record = false
id
end
The change is just to pass false as the second parameter to attributes_with_quotes, and use quoted_attributes.keys for the column names when building the SQL. This has worked for me. The downside is that by overriding this you will lose before_create and after_create callbacks, and I haven't had time to dig into it enough to figure out why. If anyone cares to expand/improve on this solution or offer a better solution, I'm all ears.