Ruby on Rails | Setting Attributes - ruby-on-rails

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.

Related

Ruby on rails: How to create single api for create and update both actions [duplicate]

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.

Rails create or update magic?

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.

Cannot assign ActiveRecord Attributes in Model

I am trying to assign values of ActiveRecord attributes within my models, but for whatever reason, I can't set them.
For instance, I have an AccountModel and this has an Attribute name
If I set it from the controller or the console (like user.name = "John"), everything works fine.
But, if I try to set it from within the model, like
def set_name(new_name)
name = new_name
end
then it doesn't work. On the other hand, retrieving the name, like
def get_name
name
end
works just fine. Am I missing something?!
I am using Ruby 2.0.0-p247 and Rails 4.0.0; Please note, that this examples aren't real world examples, I just tried to keep them simple to clarify my problem.
Best regards,
Mandi
Try:
def set_name(new_name)
self.name = new_name
end
You need to use the self keyword to refer to your instance attributes on assignment. Otherwise ruby will assign your new name to a local variable called name.
You might want to save your changes after
user = User.new
user.set_name('foo')
user.save
Take a look at the example here, there is one similar to your question at the end ;)
Your code looks fine, but are you saving the changes? Try adding a save call:
def set_name(new_name)
self.name = new_name
self.save
end
You don't need to do self.save, just call save inside your model. You only use self.attribute when you need to assign.
Try
def set_name(new_name)
self.name = new_name
self.save!
end
Then call the instance method from controller simply
user.set_name('foo')

Rails CanCan and Scoped attr_protected

I've got a Rails 3.1 app using CanCan 1.6. I'm protecting a variety of attributes with attr_protected ... :as => :api. I'd like to use load_and_authorize_resource, but also to strip off the protected fields. Any ideas?
EDIT: Here's my current workaround:
In orders#new, I'm skipping the load_resource and just authorizing. I initialize the new order directly, with the appropriate scope:
#order = Order.new(params[:order], :as => :api)
Then setting the user manually:
#order.user = current_user
Certainly not a huge deal to do, but less elegant than just letting CanCan load and authorize natively, as I'm doing elsewhere.
Generally if you only have a few protected fields to submit for, it would just be easier to pull the value out of the parameter hash and manually assign it.
##save the parameter and delete it from the hash
role = params[:employee][:role_id]
params[:employee].delete("role_id")
##create object and assign parameter manually
#employee = Employee.new(params[:employee])
#employee.role_id = role
If you have a lot of attributes, you could automate the manual assignment.
##list protected attributes and create new employee
attr = Employee.protected_attributes ##["id", "type", "name"]
#employee = Employee.new
##loop through params and manually send value
params[:employee].each do |p|
#employee.send("#{p}=", params[:employee]["#{p}"])
end
Didn't have time to fully test the second part, but that should get you in the right direction.

Accessing model properties in Rails

So basically I have a controller. something like this
def show
#user = User.find[:params[id]]
#code to show in a view
end
User has properties such as name, address, gender etc. How can I access these properties in the model? Can I overload the model accesser for name for example and replace it with my own value or concatenate something to it. Like in the show.html.erb view for this method I might want to concatenate the user's name with 'Mr.' or 'Mrs.' depending upon the gender? How is it possible?
I would hesitate to override the attributes, and instead add to the model like this:
def titled_name
"#{title} #{name}"
end
However, you can access the fields directly like this:
def name
"#{title} #{self[:name]}"
end
You can create virtual attributes within your model to represent these structures.
There is a railscast on this very subject but in summary you can do something like this in your model
def full_name
[first_name, last_name].join(' ')
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
If you wish to explicitly change the value of an attribute when reading or writing then you can use the read_attribute or write_attribute methods. (Although I believe that these may be deprecated).
These work by replacing the accessor method of the attribute with your own. As an example, a branch identifier field can be entered as either xxxxxx or xx-xx-xx. So you can change your branch_identifier= method to remove the hyphens when the data is stored in the database. This can be achieved like so
def branch_identifier=(value)
write_attribute(:branch_identifier, value.gsub(/-/, '')) unless value.blank?
end
If you are accessing data stored directly in the database you can do this in you view:
<%= #user.firstname %>
<%= #user.gender %>
etc.
If you need to build custom representations of the data, then you will either need to create helpers, or extend the model (as above).
I tend to use helper methods added to the model for things like that:
def formatted_name
"#{title} #{first_name} #{last_name}"
end
(Edit previous post. Looked back at my code and realized helpers are supposed to be for presentation-related (mark-up) stuff only.)
(Edit again to remove left-over parameter... Geez, not enough coffee this morning.)
(Edit again to replace $ with #... Perhaps I should just remove this one huh?)
You can easily overload the attributes as you suggest.
i.e. if name is a field in the users database table, you can do:
def name
"#{title} #{read_attribute[:name]}"
end
The read_attribute function will return the database column value for the field.
Caveat: I am not sure this is a good idea. If you want a method that displays model data in a modified way, I would be tempted not to overload the default methods, and call them something different - this will avoid a certain level of obfuscation.
Documentation here: http://api.rubyonrails.org/classes/ActiveRecord/Base.html (under 'Overwriting default accessors')
in http://api.rubyonrails.org/classes/ActionController/Base.html
search for
Overwriting default accessors

Resources