After getting all values from model, I want to add another custom attribute to the ActiveRecord class (this attribute is not a column in db) so that I could use it in view, but rails does not allow me to add one. What should I add in its model class?
#test.all
#test.each do |elm|
elm[:newatt] = 'added string'
end
error:
can't write unknown attribute `newatt'
try this
class Test < ActiveRecord::Base
attr_accessor :newattr
end
you can access it like
#test = Test.new
#test.newattr = "value"
As you may notice this a property, not a hash. so it uses . syntax. however, if you need it to behave like an hash you can do this without defining a new attribute
#test.all
#test.each do |elm|
new_elm = {}
new_elm[:newatt] = 'added string'
end
Lastly, I am not exactly sure what you are trying to do. if this doesn't make sense to you, kindly rephrase your question so we can understand the problem better.
Define virtual attributes as instance variables:
attr_accessor :newattr
If you want this only for your views and do not have any other purpose then you need not to add attr_accessor
#test.all.select('tests.*, "added string" as newattr')
here you are adding newattr attribute for query output of ActiveRecord with a value 'added string'
I think you mean to assign #test to the ActiveRecord query, correct? Try:
#test = MyARClass.select("*, NULL as newatt")
#test.each {|t| t[:newatt] = some_value}
Another related solution is to make it a singleton class method, though you'd have to jump though more hoops to make it writeable and I intuitively feel like this probably incurs more overhead
#test = MyARClass.all
#test.each do t
def t.newatt
some_value
end
end
Using the second method, of course you'd access it via #test.first.newatt, rather than #test.first[:newatt]. You could try redefining t.[] and t.[]=, but this is starting to get really messy.
If it's really just temporary it doesn't have to be in the object:
#test.all
#test_temp = []
#test.each do |elm|
#test_temp << {:elm => elm, :newatt => 'added string'}
end
Otherwise, there are also good answers here.
If it temporary, you can try this:
#test.all.map{ |t| t.attributes.merge({ newatt: "added string" }) }
#test.all
#test.each do |elm|
write_attribute(:newatt, "added string")
end
I met the same issue. and successfully bypass using instance_eval
#test.all
#test.each do |elm|
elm.instance_eval { #newatt = 'added string' }
end
normally it doesn't run into issue, when use attr_accessor. it appears when other DSL override "newattr=" which cause the issue. In my case, it's money-rails "monetize :newatt"
Explicitly use write_attribute doesn't work as it is the reason to raise exception in rails 4.x
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.
What is the preferred way in Rails 5 with activerecord to update the attribute only if it is currently nil.
car = Car.first
car.connected_at = Time.zone.now
car.save
OR
car = Car.first
car.update!(connected_at: Time.zone.now)
it should update only if car.connected_at is nil
You can simply check for #nil?
car = Car.first
car.update_attribute(:connected_at, Time.zone.now) if car.connected_at.nil?
That's not generic enough. I want something like before_validation etc. I am just not sure which way is the preferred one.
Well if you want to go for validation, it would look something like this..
before_save :validate_connected_at
private
def validate_connected_at
connected_at = connected_at_was if connected_at_changed? && connected_at_was.present?
end
OR
before_save :set_connected_at
private
def set_connected_at
connected_at = Time.zone.now if connected_at.nil?
end
As you can see, more checks, more methods. I would definitely go for the first one.
However, if you want to add error message, then this is the way
errors.add(:connected_at, 'Already present!')
So "#{attr}_was" is always available on all the defined attrs in before_save method?
They are available in general and not only in before_save, e.g. in the console..
car = Car.first
car.connected_at
=> 'some value'
car.connected_at = 'some other value'
car.connected_at
=> 'some other value'
car.connected_at_was
=> 'some value'
It sounds like you're saying you want to modify the behaviour of how a particular attribute works so it quietly ignores you. I think the instinct behind why you want to seal this off is reasonable one but if you think about it a bit more you might consider that if you do this kind of thing in a lot of places then using your objects will start to become confusing particularly for someone else who doesn't know the code well.
Perhaps you want to do this because there's other code using the Car model that wants to make connections but doesn't really have the full picture so it tries stuff which you only want to succeed the first time. It's much better to handle such operations solely inside a class which does have the full picture such as the Car model or a service object.
If you still really want to control this "connecting" behaviour outside the Car then you can override the attr_writer completely in the Car class. I'd definitely recommend doing this on before_save callback instead though.
def connected_at=(new_value)
if #connected_at
raise StandardError, 'connected_at has already been set'
end
#connected_at = new_value
end
That will work whichever way you try to assign the value. If you're wondering about what's going on above have a read about attr_accessor in ruby.
this is my understanding of your question.
Car can update only if connected_at is nil
class Car < ApplicationRecord
before_save :updatable?
def updatable?
connected_at.blank?
end
end
The point is return false when before_save.
You could:
car = Car.first
car.connected_at ||= Time.zone.now
car.save
That will only assign if connected_at is nil of false.
I would propose to use the before_update callback and rephrase the intention of the OP as "discard updates if my attribute already has a value".
I came up with this solution (which works well with mass assignments such as Car.update(car_params)):
before_update :ignore_updates_to_connected_at
def ignore_updates_to_connected_at
return unless connected_at.present? && connected_at_changed?
clear_attribute_change(:connected_at)
end
The <attribute_name>_changed? and clear_attribute_change methods come from ActiveModel::Dirty.
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.
I'm using this regex in my model to validate an URL submitted by the user. I don't want to force the user to type the http part, but would like to add it myself if it's not there.
validates :url, :format => { :with => /^((http|https):\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+).[a-z]{2,5}(:[0-9]{1,5})?(\/.)?$/ix, :message => " is not valid" }
Any idea how I could do that? I have very little experience with validation and regex..
Use a before filter to add it if it is not there:
before_validation :smart_add_url_protocol
protected
def smart_add_url_protocol
unless url[/\Ahttp:\/\//] || url[/\Ahttps:\/\//]
self.url = "http://#{url}"
end
end
Leave the validation you have in, that way if they make a typo they can correct the protocol.
Don't do this with a regex, use URI.parse to pull it apart and then see if there is a scheme on the URL:
u = URI.parse('/pancakes')
if(!u.scheme)
# prepend http:// and try again
elsif(%w{http https}.include?(u.scheme))
# you're okay
else
# you've been give some other kind of
# URL and might want to complain about it
end
Using the URI library for this also makes it easy to clean up any stray nonsense (such as userinfo) that someone might try to put into a URL.
The accepted answer is quite okay.
But if the field (url) is optional, it may raise an error such as undefined method + for nil class.
The following should resolve that:
def smart_add_url_protocol
if self.url && !url_protocol_present?
self.url = "http://#{self.url}"
end
end
def url_protocol_present?
self.url[/\Ahttp:\/\//] || self.url[/\Ahttps:\/\//]
end
Preface, justification and how it should be done
I hate it when people change model in a before_validation hook. Then when someday it happens that for some reason models need to be persisted with save(validate: false), then some filter that was suppose to be always run on assigned fields does not get run. Sure, having invalid data is usually something you want to avoid, but there would be no need for such option if it wasn't used. Another problem with it is that every time you ask from a model is it valid these modifications also take place. The fact that simply asking if a model is valid may result in the model getting modified is just unexpected, perhaps even unwanted. There for if I'd have to choose a hook I'd go for before_save hook. However, that won't do it for me since we provide preview views for our models and that would break the URIs in the preview view since the hook would never get called. There for, I decided it's best to separate the concept in to a module or concern and provide a nice way for one to apply a "monkey patch" ensuring that changing the fields value always runs through a filter that adds a default protocol if it is missing.
The module
#app/models/helpers/uri_field.rb
module Helpers::URIField
def ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
In your model
extend Helpers::URIField
ensure_valid_protocol_in_uri :url
#Should you wish to default to https or support other protocols e.g. ftp, it is
#easy to extend this solution to cover those cases as well
#e.g. with something like this
#ensure_valid_protocol_in_uri :url, "https", "https?|ftp"
As a concern
If for some reason, you'd rather use the Rails Concern pattern it is easy to convert the above module to a concern module (it is used in an exactly similar way, except you use include Concerns::URIField:
#app/models/concerns/uri_field.rb
module Concerns::URIField
extend ActiveSupport::Concern
included do
def self.ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
end
P.S. The above approaches were tested with Rails 3 and Mongoid 2.
P.P.S If you find this method redefinition and aliasing too magical you could opt not to override the method, but rather use the virtual field pattern, much like password (virtual, mass assignable) and encrypted_password (gets persisted, non mass assignable) and use a sanitize_url (virtual, mass assignable) and url (gets persisted, non mass assignable).
Based on mu's answer, here's the code I'm using in my model. This runs when :link is saved without the need for model filters. Super is required to call the default save method.
def link=(_link)
u=URI.parse(_link)
if (!u.scheme)
link = "http://" + _link
else
link = _link
end
super(link)
end
Using some of the aforementioned regexps, here is a handy method for overriding the default url on a model (If your ActiveRecord model has an 'url' column, for instance)
def url
_url = read_attribute(:url).try(:downcase)
if(_url.present?)
unless _url[/\Ahttp:\/\//] || _url[/\Ahttps:\/\//]
_url = "http://#{_url}"
end
end
_url
end
I had to do it for multiple columns on the same model.
before_validation :add_url_protocol
def add_url_protocol
[
:facebook_url, :instagram_url, :linkedin_url,
:tiktok_url, :youtube_url, :twitter_url, :twitch_url
].each do |url_method|
url = self.send(url_method)
if url.present? && !(%w{http https}.include?(URI.parse(url).scheme))
self.send("#{url_method.to_s}=", 'https://'.concat(url))
end
end
end
I wouldn't try to do that in the validation, since it's not really part of the validation.
Have the validation optionally check for it; if they screw it up it'll be a validation error, which is good.
Consider using a callback (after_create, after_validation, whatever) to prepend a protocol if there isn't one there already.
(I voted up the other answers; I think they're both better than mine. But here's another option :)
How can I pass a method with an argument to #to_xml?
#object.to_xml(:methods => [:a_method_with_args] )
Is there a way to do this? What is the correct syntax?
Thanks.
to_xml is supposed to express your model's state. and as such it shouldn't need any external 'location' arguments. if this is really what you need it looks like you need a 'give me an xml representation of my model when on location X'. I guess you can just add a 'set_default_location' to your model and change the price_points_for_location to have a default value for the argument:
attr_writer :default_location
def price_points_for_location(location = #default_location)
...
end
You could try redefining the to_xml method like
def to_xml(location)
# do your stuff
super()
end
But not sure it would work that well. Other option would be to create some new XML view method for your model, like:
def as_xml(location)
self.price_points_for_location(location)
self.to_xml
end
Thanks for the answers, they look like good options. What I actually ended up doing is using a proc. I was aware that I could use procs with to_xml, but it seems that you can't access the current object in the array when iterating over multiple objects. To get around this I did something like this:
price_points = #items.map { |item| item.price_points_for_location(location) }
price_point = Proc.new {|options| options[:builder].tag!('price_points', price_points.shift) }
#items.to_xml(:procs => [price_point])