Rails merge temporary attributes to model object - ruby-on-rails

I have a Rails model object that has attributes maps to columns in a DB. I compute couple of temporary fields name and age which are not in the DB columns. Is there a way to merge them into the object temporarily without making changes to the model object?
class MyModel < ApplicationRecord
end
I tried using attributes.merge. and tried :
my_obj = my_obj.attributes.merge({ age: compute_age })
my_obj = my_obj.attributes.merge({ name: compute_name })
The second merge however fails with NoMethodError: undefined method `attributes' for #Hash:0x0000000120060010.
Looks like the moment I merge first one it makes the object a Hash. So I need to do
my_obj = my_obj.attributes.merge({ age: compute_age })
my_obj[:name]=compute_name
All the other properties also are accessed as a hash.
However this feels inconsistent and weird to me!
Is there a better option to this than using attributes.merge i.e preserve the object?
I guess it maybe cleaner to add the attr to the model object but those attributes are temporary and should not be accessed outside.

As soon as you call .attributes you turn the MyModel instance into a hash.
To turn it back into a model you can just pass that hash to the MyModel constructor (assuming you have an attr_accessor for the virtual attribute):
my_obj = MyModel.new(my_obj.attributes.merge(age: compute_age))
my_obj = MyModel.new(my_obj.attributes.merge(name: compute_name))
You can also see here for some other options: Rails update_attributes without save?
edit
to clarify a little more. You can turn a model instance into a hash via .attributes. You can turn it back into a model using Model.new(attributes_hash). While it's a hash, you can set whatever key-vals you want - it's just a hash. But in order to convert it back to a model instance, you need to make sure that all the hash keys correspond to actual attributes on the model (be they persisted attributes defined by the schema, or virtual attributes defined by attr_accessor).
If you just want to turn it into a hash and add multiple arbitrary keyvals there, you can do it with a single merge:
attrs = my_obj.attributes.merge({ age: compute_age, name: compute_name })

Related

Undefined method for NilClass in rails console

I've a model named "ListingInfo", whenever I try to generate new object from this model, it shows the "undefined method for nilclass". I'm unable to add its attributes other than id.
I've also tried "create" method but on running "ListingInfo.count.all", it still returns zero(please see second image for more clarity). Means, it is still not being saved to database.
You need to create the object. Using ListingInfo.new will only instantiate it but won't validate it or save it to the DB. Also you shouldn't pass a value on the id that's generated automatically from Rails.
Solutions:
Use new and then save the object
listing = ListingInfo.new
listing.save
Use create
ListingInfo.create
ActiveRecord::Base documentation
create(attributes = nil) {|object| ...}
Creates an object (or multiple objects) and saves it to the database,
if validations pass. The resulting object is returned whether the
object was saved successfully to the database or not.
new(attributes = nil) {|self if block_given?| ...}
New objects can be instantiated as either empty (pass no construction
parameter) or pre-set with attributes but not yet saved (pass a hash
with key names matching the associated table column names). In both
instances, valid attribute keys are determined by the column names of
the associated table — hence you can‘t have attributes that aren‘t
part of the table columns.

Updating if exist or create if not rails

So im using an api to get info on weather, its executes everyday, what im trying to do is to get updated if already exist, and create a new one if it doesn't in table.
I do want to update all attributs when udpdating.
i did try
model = Model.where(column_name: value).first_or_initialize(locked: false)
but i get an error saying :
unknown attribute locked for Model
raise UnknownAttributeError.new(self ,k.to_s)
If you need anything, ask and i will comment or edit. Im newb to ruby and rails
Firstly, the model.Model part should be just Model, as Model is your class.
locked is supposed to be a column/attribute of the Model class, although it seems is not the case judging from your error. Therefore, I'm gonna use other_column_name as an example.
Explanation of what this is doing:
Model.where(column_name: value).first_or_initialize(other_column_name: some_value)
Model.where(column_name: value): gets models that satisfy the condition column_name == value
first_or_initialize: if a model such that column_name == value was found, that one is returned. Otherwise, it initializes a model with column_name = value.
By passing other_column_name: some_value, if the model was not found and needs to be initialized, it sets other_column_name to some_value but: 1) it does not update it if it was initially found and 2) it does not save the record.
The equivalent of first_or_initialize that saves the new record would be first_or_create but this would still not update the record if it already existed.
So, you should do something like this:
m = Model.where(column_name: value).first_or_initialize
m.other_column_name = some_value
m.save
This way, you first get a model where column_name is value or initialize a new one with this value if it didn't already exist. Then, you set the attribute other_column_name to some_value and save the model.
A one-liner alternative would be
Model.where(column_name: value).first_or_create.update(other_column_name: some_value)
However, note that if it needs to be created, this one will perform 2 queries (the insert and the update).
About the error part. It says the attribute locked does not exist on the Model record. Are these classes you created? Are you using some pre-existing project? You could try posting Model.attribute_names and maybe your schema.rb
Firstly refer to the docs here
A table by the name of weather with the following attributes location: string temperature:integer wind:string needing to be updated or initialized based on the location would work like this
#weather_record = Weather.find_or_initialize_by(location: location_value)
#weather.temperature = -60
#weather.wind = strong
#weather.save
Next, never, ever use a reserved name for a model so do not have Model as the name of your table
Lastly in your example
model.Model.where(column_name: value).first_or_initialize(locked: false)
you are saying
a_record.ClassName.where which is just wrong, If you are using a class method then start with the class name e.g. Weather.where if you are using instance methods then use the instance name e.g. an_instance_of_weather.some_field
Try this mate:
column_name_value = (Way that you get the info from data)
model = Model.find_or_initialize_by column_name: column_name_value
Let me know if worked!

Rails + MongoDB not returning all documents in collection

I need all the documents in one of my collections to create association in between my parent model to child. The problem is I only have the string of my ObjectId. So I am finding the object by string and then set via parent.child = foundObject. So, to achieve this I created a private method as below, to not to create DB request each time I need that child object.
def childs
#childs ||= Child.all
end
But this is not working as expected. When I run ModelName.all it returns below result; not all the docs in collection.
=>
#<Mongoid::Criteria
selector: {}
options: {}
class: ModelName
embedded: false>
And this causes my loop to create another DB request each time I try to associate child to parent. I prevent this by using below method.
def childs
#childs ||= Child.all.select { |v| v.id.present? }
end
I believe there should be a way of collecting all documents in MongoDB, I know the idea of Mongoid::Criteria and what it actually does. But in some case, I need all the objects to be stored in one variable. Do not want to create unwanted DB queries each time I need one specific document in a model.
I could not find a way to solve this specific problem and I think it's kind of impossible since MongoDB is not a relational DB It's quite hard to collect information at the same time with querying. What I used is "MongoDB views" and this solved a lot. Here is the docs. There you can read and find yourself an approach to figure out your own problem.

Using Variables for Model Names in Active Record

I am writing a seed file that will make several API calls via HTTParty in order to populate the database. I am pulling the same information for several different models and I would like to be able to use a single method for all of them. However, I cannot figure out how to reference the model name through a variable. Specifically I am having difficulties because each of these must belong to another model. I have tried the following:
def create_assets(subject, model, geokit_hoods)
response = HTTParty.get("https://raw.githubusercontent.com/benbalter/dc-maps/master/maps/#{subject}.geojson")
parsed = JSON.parse(response)
collection = parsed["features"]
collection.each do |station|
coordinates = station["geometry"]["coordinates"].reverse
point = Geokit::LatLng.new(coordinates[0], coordinates[1])
geokit_hoods.each do |hood|
if hood[1].contains?(point)
hood[0][model].create(coordinates: coordinates, name: station["properties"]["NAME"], address: station["properties"]["ADDRESS"])
break
end
end
end
end
Which I called via the following:
create_assets("metro-stations-district", "metros", geokit_hoods)
hood[0] refers to an existing neighborhood model, and hood[1] is the polygon associated with that neighborhood. The code works when referring to hood[0].metros.create(...), but I am looking for a way to make this method useful across many models.
Any ideas would be appreciated!
For now I'm going to assume that what you have in the variable is a String that is the name of the class in table-name format. eg in your example you have metros in the variable... from that I assume you have a Metro class which you are trying to create.
If so... you first need to convert your lowercase table-name style variable ("metros") into a class name-style eg "Metro"
Note: this is title cased and singular (rather than plural).
Rails has a method to do this to strings for exactly the purpose you want: classify eg you could use it thus:
model_name = hood[0][model] # 'metros'
model_name.classify # 'Metro'
Note that it's still just a string, and you can't call create on a string.. so how do you make it the real class? constantize
Use this to turn the string into the actual model-class you're trying to find... which you can then call create on eg:
model_name = hood[0][model] # 'metros'
the_klass = model_name.classify.constantize # Metro
your_instance = the_klass.create(...)

How to update table row from values determined within controller

ruby_on_rails rails 4 assignment non-screen data to insert record
Rather than using screen values (e.g. simple_form_for #user_evaluation_result) to populate the columns to insert a row I need to calculate some of the values in controller.
For example if I have these statements in the controller
….
# which if I had simple_form_for user_evaluation_result would get populated by the screen
#user_evaluation_result = UserEvaluationResult.new(user_evaluation_result_params)
….
# standard stuff I use for screen derived updates
def user_evaluation_result_params
params.require(:user_evaluation_result).
permit(:evaluation_assumption_id,
:company_listing_id,
:target_share_price_dollars )
end
How do I assign values to :user_assumption_id etc so that insert works. I have tried all sorts of statements. Alternatively do I use another format instead of calling "def user_evaluation_result_params".
Thanks in advance - Pierre
I'm hoping I've interpreted the question properly...
First, to make sure we're on the same page... The code inside of your user_evaluation_result_params method is using Strong Parameters to create an ActionController::Parameters object for the purpose of protecting your model from unpermitted mass-assignments. So, in general, when you're creating or updating an ActiveRecord object from a form in a view template, you want to use Strong Parameters so that users can't manipulate your form to set attributes that you're not expecting.
That said, if you want to set attributes on an object you don't have to use mass assignment. Here is an example of using one-at-a-time assignment (the opposite of mass-assignment):
obj = MyObject.new
obj.attr_one = "One"
obj.attr_two = "Two"
obj.save
There is nothing wrong with this approach other than that it's kind of a lot of work for the general case. So mass-assignment just saves us from having to do this all the time. But it sounds like this one-at-a-time assignment is what you're wanting in this case. So try something like this:
def create
#user_evaluation_result = UserEvaluationResult.new
# assuming you have a UserAssumption object instance in `my_user_assumption`
#user_evaluation_result.user_assumption = my_user_assumption
#user_evaluation_result.some_other_attr = "some value"
#user_evaluation_result.save
end
Note, instead of setting #user_evaluation_result.user_assumption_id directly, as you asked about, it is preferred to set the actual object association as I did above. Try to keep associations outside of mass-assignment and use object relationships to build up your object graphs.
Or, if you have some attributes coming from a form you can mix and match the two approaches:
def create
#user_evaluation_result = UserEvaluationResult.new(user_evaluation_result_params)
# assuming you have a UserAssumption object instance in `my_user_assumption`
#user_evaluation_result.user_assumption = my_user_assumption
#user_evaluation_result.some_other_attr = params[:user_evaluation_result][:some_other_attr]
#user_evaluation_result.save
end
private
def user_evaluation_result_params
params.require(:user_evaluation_result)
.permit(:evaluation_assumption_id,
:company_listing_id,
:target_share_price_dollars)
end

Resources