I have a class called CachedObject that stores generic serialized objects indexed by a key. I want this class to implement a create_or_update method. If an object is found it will update it, otherwise it will create a new one.
Is there a way to do this in Rails or do I have to write my own method?
Rails 6
Rails 6 added an upsert and upsert_all methods that deliver this functionality.
Model.upsert(column_name: value)
[upsert] It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Rails 5, 4, and 3
Not if you are looking for an "upsert" (where the database executes an update or an insert statement in the same operation) type of statement. Out of the box, Rails and ActiveRecord have no such feature. You can use the upsert gem, however.
Otherwise, you can use: find_or_initialize_by or find_or_create_by, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.
For example, if no user is found with the name "Roger", a new user instance is instantiated with its name set to "Roger".
user = User.where(name: "Roger").first_or_initialize
user.email = "email#example.com"
user.save
Alternatively, you can use find_or_initialize_by.
user = User.find_or_initialize_by(name: "Roger")
In Rails 3.
user = User.find_or_initialize_by_name("Roger")
user.email = "email#example.com"
user.save
You can use a block, but the block only runs if the record is new.
User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end
User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end
If you want to use a block regardless of the record's persistence, use tap on the result:
User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "email#example.com"
user.save
end
In Rails 4 you can add to a specific model:
def self.update_or_create(attributes)
assign_or_new(attributes).save
end
def self.assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
and use it like
User.where(email: "a#b.com").update_or_create(name: "Mr A Bbb")
Or if you'd prefer to add these methods to all models put in an initializer:
module ActiveRecordExtras
module Relation
extend ActiveSupport::Concern
module ClassMethods
def update_or_create(attributes)
assign_or_new(attributes).save
end
def update_or_create!(attributes)
assign_or_new(attributes).save!
end
def assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecordExtras::Relation
The magic you have been looking for has been added in Rails 6
Now you can upsert (update or insert).
For single record use:
Model.upsert(column_name: value)
For multiple records use upsert_all :
Model.upsert_all(column_name: value, unique_by: :column_name)
Note:
Both methods do not trigger Active Record callbacks or validations
unique_by => PostgreSQL and SQLite only
Add this to your model:
def self.update_or_create_by(args, attributes)
obj = self.find_or_create_by(args)
obj.update(attributes)
return obj
end
With that, you can:
User.update_or_create_by({name: 'Joe'}, attributes)
Old question but throwing my solution into the ring for completeness.
I needed this when I needed a specific find but a different create if it doesn't exist.
def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
obj = self.find_or_initialize_by(args)
return obj if obj.persisted?
return obj if obj.update_attributes(attributes)
end
By chaining find_or_initialize_by and update, this can be achieved in a simple way which avoids the (in my experience, often) unwanted caveats of upsert, and also minimises database calls.
For example:
Class.find_or_initialize_by(
key: "foo",
...
).update(
new_attribute: "bar",
...
)
will return you newly created or updated object.
It is worth noting that if your find_or_initialize_by attributes match multiple Class instances, only the 'first' one will be selected and updated.
You can do it in one statement like this:
CachedObject.where(key: "the given key").first_or_create! do |cached|
cached.attribute1 = 'attribute value'
cached.attribute2 = 'attribute value'
end
The sequel gem adds an update_or_create method which seems to do what you're looking for.
Related
I have a class called CachedObject that stores generic serialized objects indexed by a key. I want this class to implement a create_or_update method. If an object is found it will update it, otherwise it will create a new one.
Is there a way to do this in Rails or do I have to write my own method?
Rails 6
Rails 6 added an upsert and upsert_all methods that deliver this functionality.
Model.upsert(column_name: value)
[upsert] It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Rails 5, 4, and 3
Not if you are looking for an "upsert" (where the database executes an update or an insert statement in the same operation) type of statement. Out of the box, Rails and ActiveRecord have no such feature. You can use the upsert gem, however.
Otherwise, you can use: find_or_initialize_by or find_or_create_by, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.
For example, if no user is found with the name "Roger", a new user instance is instantiated with its name set to "Roger".
user = User.where(name: "Roger").first_or_initialize
user.email = "email#example.com"
user.save
Alternatively, you can use find_or_initialize_by.
user = User.find_or_initialize_by(name: "Roger")
In Rails 3.
user = User.find_or_initialize_by_name("Roger")
user.email = "email#example.com"
user.save
You can use a block, but the block only runs if the record is new.
User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end
User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end
If you want to use a block regardless of the record's persistence, use tap on the result:
User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "email#example.com"
user.save
end
In Rails 4 you can add to a specific model:
def self.update_or_create(attributes)
assign_or_new(attributes).save
end
def self.assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
and use it like
User.where(email: "a#b.com").update_or_create(name: "Mr A Bbb")
Or if you'd prefer to add these methods to all models put in an initializer:
module ActiveRecordExtras
module Relation
extend ActiveSupport::Concern
module ClassMethods
def update_or_create(attributes)
assign_or_new(attributes).save
end
def update_or_create!(attributes)
assign_or_new(attributes).save!
end
def assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecordExtras::Relation
The magic you have been looking for has been added in Rails 6
Now you can upsert (update or insert).
For single record use:
Model.upsert(column_name: value)
For multiple records use upsert_all :
Model.upsert_all(column_name: value, unique_by: :column_name)
Note:
Both methods do not trigger Active Record callbacks or validations
unique_by => PostgreSQL and SQLite only
Add this to your model:
def self.update_or_create_by(args, attributes)
obj = self.find_or_create_by(args)
obj.update(attributes)
return obj
end
With that, you can:
User.update_or_create_by({name: 'Joe'}, attributes)
Old question but throwing my solution into the ring for completeness.
I needed this when I needed a specific find but a different create if it doesn't exist.
def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
obj = self.find_or_initialize_by(args)
return obj if obj.persisted?
return obj if obj.update_attributes(attributes)
end
By chaining find_or_initialize_by and update, this can be achieved in a simple way which avoids the (in my experience, often) unwanted caveats of upsert, and also minimises database calls.
For example:
Class.find_or_initialize_by(
key: "foo",
...
).update(
new_attribute: "bar",
...
)
will return you newly created or updated object.
It is worth noting that if your find_or_initialize_by attributes match multiple Class instances, only the 'first' one will be selected and updated.
You can do it in one statement like this:
CachedObject.where(key: "the given key").first_or_create! do |cached|
cached.attribute1 = 'attribute value'
cached.attribute2 = 'attribute value'
end
The sequel gem adds an update_or_create method which seems to do what you're looking for.
Is there an equivalent of Active Records Model.create_with to pass creation parameters separate of find parameters in Mongoid?
# Find the first user named "Scarlett" or create a new one with
# a particular last name.
User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
# => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
I find myself using a clunky workaround:
user = User.find_or_initialze_by(first_name: 'Scarlett')
user.update(last_name: 'Johansson') if user.new_record?
Mongoid's find_or_create_by takes an optional block which is only used when it needs to create something. The documentation isn't exactly explicit about this behavior but if you check the code you'll see that find_or_create_by ends up as a call to this find_or method:
def find_or(method, attrs = {}, &block)
where(attrs).first || send(method, attrs, &block)
end
with method being :create and the block isn't used if the document you're looking for is found by where.
That means that you can say:
user = User.find_or_create_by(first_name: 'Scarlett') do |user|
user.last_name = 'Johansson'
end
to get the effect you're after.
Presumably this "the create half uses the block" behavior is supposed to be obvious because create takes a block to initialize the object but find doesn't.
If you're paranoid about this undocumented behavior, you can include a check for it in your specs so you'll at least know when an upgrade breaks it.
In controller my code is
query = "insert into users (name, email,updated_at) values (#{name},#{email},now()) ON DUPLICATE KEY UPDATE updated_at=now()"
User.connection.execute(query)
and in model
after_create :change_updated_at
def change_updated_at
if !self.email.blank?
chk_user = User.find_by_id(self.email)
if !chk_user.blank? && chk_user.updated_at.blank?
chk_user.updated_at =Time.now
chk_user.save!
end
end
end
but it's not working please help
Your query should be replaced by something like this. This will provide the same functionality as using ON DUPLICATE KEY UPDATE
user = User.where("name = ? and email = ?", name, email)
if user.nil?
user = User.new
end
user.name = name
user.email = email
user.save
Since you want this function to run even if it is an update. You want to use the after_save callback http://apidock.com/rails/ActiveRecord/Callbacks/after_save
The after_create is called only on creation of brand new objects.
http://apidock.com/rails/ActiveRecord/Callbacks/after_create
List of all callbacks provided by active record are here
http://apidock.com/rails/ActiveRecord/Callbacks
Don't run your query directly, that's VERY un-rails like. Let rails generate the query for you.
By running it yourself, rails has no way of knowing that you've created the record, so the after_create filter isn't be called.
Change your code to something like :
User.create(name: name, email: email)
Then it'll run. Also, don't update the 'create_at' field yourself. If you use rails methods, it'll do this automatically for you as well :)
You need to get with params object input attributes.
So first of all you need new method, and then create method (if you use standart form helper for model):
def create
#user=User.new(params[:user])
#other code
end
Are there any other ways to set attributes for models in Rails other than using attribute=?
For example, is there something like set_attribute(name, value)?
user = User.new
user.set_attribute(:name, 'Jack')
user.set_attribute(:surname, 'The Ripper')
user.save
# instead of
user.name = 'Jack'
user.surname = 'The Ripper'
looking to the AR source we can find
# File activerecord/lib/active_record/persistence.rb, line 208
def update_attributes(attributes, options = {})
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
self.assign_attributes(attributes, options)
save
end
end
so you can use assign_attributes(attributes, options) to set attributes without saving
Also if you want to set attribute by name without calling a method directly you can use
user.send(:name=, 'Jack') instead of user.set_attribute(:name, 'Jack')
There is a write_attribute method. It should be what you're looking for.
EDIT
You can use update_attributes if you want to update many attributes at once.
I have a model User and when I create one, I want to pragmatically setup some API keys and what not, specifically:
#user.apikey = Digest::MD5.hexdigest(BCrypt::Password.create("jibberish").to_s)
I want to be able to run User.create!(:email=>"something#test.com") and have it create a user with a randomly generated API key, and secret.
I currently am doing this in the controller, but when I tried to add a default user to the seeds.rb file, I am getting an SQL error (saying my apikey is null).
I tried overriding the save definition, but that seemed to cause problems when I updated the model, because it would override the values. I tried overriding the initialize definition, but that is returning a nil:NilClass and breaking things.
Is there a better way to do this?
use callbacks and ||= ( = unless object is not nil ) :)
class User < ActiveRecord::Base
before_create :add_apikey #or before_save
private
def add_apikey
self.apikey ||= Digest::MD5.hexdigest(BCrypt::Password.create(self.password).to_s)
end
end
but you should definitely take a look at devise, authlogic or clearance gems
What if, in your save definition, you check if the apikey is nil, and if so, you set it?
Have a look at ActiveRecord::Callbacks & in particular before_validation.
class User
def self.create_user_with_digest(:options = { })
self.create(:options)
self.apikey = Digest::MD5.hexdigest(BCrypt::Password.create("jibberish").to_s)
self.save
return self
end
end
Then you can call User.create_user_with_digest(:name => "bob") and you'll get a digest created automatically and assigned to the user, You probably want to generate the api key with another library than MD5 such as SHA256 you should also probably put some user enterable field, a continuously increasing number (such as the current date-time) and a salt as well.
Hope this helps
I believe this works... just put the method in your model.
def apikey=(value)
self[:apikey] = Digest::MD5.hexdigest(BCrypt::Password.create("jibberish").to_s)
end