I'm working on a rake task which imports from a JSON feed into an ActiveRecord called Person.
Person has quite a few attributes and rather than write lines of code for setting each attribute I'm trying different methods.
The closest I've got is shown below. This works nicely as far as outputing to screen but when I check the values have actually been set on the ActiveRecord itself it's always nil.
So it looks like I can't use .to_sym to solve my problem?
Any suggestions?
I should also mention that I'm just starting out with Ruby, have been doing quite a bit of Objective-c and now need to embrace the Interwebs :)
http = Net::HTTP.new(url.host, url.port)
http.read_timeout = 30
json = http.get(url.to_s).body
parsed = JSON.parse(json)
if parsed.has_key? 'code'
updatePerson = Person.find_or_initialize_by_code(parsed['code'])
puts updatePerson.code
parsed.each do |key, value|
puts "#{key} is #{value}"
symkey = key.to_sym
updatePerson[:symkey] = value.to_s
updatePerson.save
puts "#{key}....." # shows the current key
puts updatePerson[:symkey] # shows the correct value
puts updatePerson.first_name # a sample key, it's returning nil
end
You're probably looking for update_attributes():
if parsed.has_key?('code')
code = parsed.delete('code')
person = Person.find_or_initialize_by_code(code)
if person.update_attributes(parsed)
puts "#{person.first_name} successfully saved"
else
puts "Failed to save #{person.first_name}"
end
end
Your code can not assign any attribute, because you are always assigning to the single attribute named "symkey":
symkey = key.to_sym
updatePerson[:symkey] = value.to_s # assigns to attribute "symkey", not to the attribute with the name stored in variable symkey
If you want to make key into a symbol (which is probably not even necessary) and then use that as an index to access the attribute in updatePerson, you can write:
updatePerson[key.to_sym] = value.to_s
updatePerson.save
But this - more or less - is the same as
updatePerson.updateAttribute(key.to_sym, value.to_s) # update and save
except that no validation is triggered, so use with care.
And performancewise it might not be such a good idea to save the person after each assignment, so maybe you want to defer the .save() call until after you have assigned all attributes.
Nevertheless, updateAttributes(...) is something you might want to be looking into - if you do, do not forget to inform yourself on attr_protected or attr_accessible, as they protect attributes from "bulk assignment"
You can use write_attribute:
parsed.each do |key, value|
updatePerson.write_attribute(key, value)
end
updatePerson.save
Related
I am making an application, part of whose code requires many if .. else conditions:
if #model_name == "Style"
if row.include? ('colors')
colors = row['colors'].split(';')
model.style_colors.concat Color.where('code IN (?)', colors).map {|i| i.id.to_s }
row.delete('colors')
end
if row.include? ('gender') and row.include? ('garments')
#garments = row['garments']
#gender = row['gender']
row.delete('garments')
row.delete('gender')
end
if row.include? ('sports')
#sports = row['sports']
row.delete('sports')
end
if row.include?('decoration_packages')
#decorations_packages = row['decoration_packages']
row.delete('decoration_packages')
end
model.attributes = row.to_hash.merge!(active: FALSE)
else
model.attributes = row.to_hash
end
I need to make objects of row hash to access subclasses, and then delete them from row so it can be saved to a model.
Any idea how I can minimize the use of conditions or optimize it?
There's a few optimisations here...
row.include? ('gender') and row.include? ('garments')
could be implemented as
['gender', 'garments'].all?{|x| row.include?(x)}
#garments = row['garments']
row.delete('garments')
could be implemented as
#garments = row.delete('garments')
You could actually squash a lot of these onto one line:
if row.include? ('sports')
#sports = row['sports']
row.delete('sports')
end
could be
#sports = row.delete('sports') if row.include? ('sports')
Also worth considering:
Do you need to delete the values from 'row'? Could you just retrieve the value?
What are you trying to do here? It looks like you're pulling a hash into instance variables... Which is what ActiveRecord does, basically. Could you just create a model with these attributes and then call it in this style?
Style.new(row)
if #model_name == "Style"
if row.include?('colors')
model.style_colors.concat(
Color.where(code: row.delete('colors').split(';')).pluck(:id).map(&:to_s)
)
end
if row.include?('gender') and row.include?('garments')
#garments = row.delete('garments')
#gender = row.delete('gender')
end
if row.include?('sports')
#sports = row.delete('sports')
end
if row.include?('decoration_packages')
#decorations_packages = row.delete('decoration_packages')
end
model.attributes = row.to_hash.merge!(active: false)
else
model.attributes = row.to_hash
end
I would do something like this with your current code:
if #model_name == "Style"
row_key_set = row.keys.to_set
if row.include? 'colors'
colors = row['colors'].split(';')
color_ids = Color.where(code: colors).pluck(:id)
model.style_colors.concat(color_ids.map(&:to_s))
end
if row_key_set >= Set['gender', 'garments']
#garments = row.delete('garments')
#gender = row.delete('gender')
end
#sports = row.delete('sports')
#decorations_packages = row.delete('decoration_packages')
model.attributes = row.to_hash.merge(active: false)
else
model.attributes = row.to_hash
end
Instead of using Color.where('code IN (?)', colors) you can just use Color.where(code: colors).
Instead of using .map {|i| i.id.to_s } you can use pluck (.pluck(:id)) to get an array of color ids. This also makes for a quicker query since only the ids get fetched from the database instead of the whole records.
I personally like to use sets to check if multiple values are present in another set. For this reason I create the row_key_set variable row.keys.to_set. Now you can easily check certain keys are present on your hash by just checking if the key set is greater or equal than another set (thus being a superset). row_key_set >= Set['gender', 'garments'] With just one check you could leave this out, but if you have multiple checks this might be worth the trouble. I also find code written this way also more readable, but that's just personal peference.
You don't need to check if a key is present on a Hash, the documentation tells us the following:
Deletes the key-value pair and returns the value from hsh whose key is equal to key. If the key is not found, it returns nil.
This means you can leave out the include? check and write the result from the delete directly to the instance variable. If the key is not present nil will be set for the instance variable.
Lastly I would leave out the explanation mark in row.to_hash.merge!(active: false). The version without explanation mark doesn't alter the original array and reduces the chance on accidental side effects. You're saving the variable to model.attributes anyway and toss away the generated array from the to_hash method. It's normally better to use non-altering versions of methods, unless you explicitly want a certain effect to happen.
So in my past application, I was somewhat familiar with using .includes in Rails, but for some reason I'm having a bit of a difficult time in my current scenario.
Here's what I'm working with:
# If non-existent, create. Otherwise, update.
existing_data = Page.all
updated_data = {}
new_records = []
#latest_page_data.each do |key, value|
existing_record = existing_data.find_by(symbol: key)
if existing_record != nil
updated_data[existing_record.id] = value
else
new_records << Page.new(value)
end
end
if !new_records.empty?
Page.import new_reocrds
end
if !updated_data.empty?
Page.update(updated_data.keys, updated_data.values)
end
end
The problem that I'm having is that the .find_by portion of the code results in a query every single iteration of #latest_page_data. I guess I would think that existing_data would hold all of the data it needs in memory, but obviously it doesn't work that way.
So next, I tried something like this:
# If non-existent, create. Otherwise, update.
existing_data = Page.includes(:id, :symbol)
updated_data = {}
new_records = []
#latest_currency_data.each do |key, value|
existing_record = existing_data.find_by(symbol: key)
but then rails throws an error, stating:
ActiveRecord::AssociationNotFoundError (Association named 'id' was not
found on Page; perhaps you misspelled it?):
so I can't use this example to find the id and symbol attributes.
I tried to take out :id in the Page.includes method, but I need to be able to get to the ID attribute in order to update the respective record later down in the code.
I've also saw some other posts pertaining to this topic, but I think the problem I may be running into is I'm not dealing with associations (and I believe that's what .includes is for? If this is the case, is there any other way that I can reduce all of the queries that I'm submitting here?
The includes method is used to preload associated models. I think what you are looking for is a select. Modifying your code to use select, do this :
existing_data = Page.select(:id, :symbol).load
updated_data = {}
new_records = []
#latest_currency_data.each do |key, value|
existing_record = existing_data.find_by(symbol: key)
if existing_record
updated_data[existing_record.id] = value
else
new_records << Page.new(value)
end
end
The drawbacks of using select over pluck is that since Rails constructs an object for you, so it is slower than a pluck. Benchmark: pluck vs select
Rather than trying to figure out a way to do it in Rails (since I'm not familiar with the 100% correct/accurate Rails way), I just decided to use .pluck and convert it into a hash to get the data that I'm looking for:
existing_data = Page.pluck(:id, :symbol)
existing_data = Hash[*existing_data.flatten]
updated_data = {}
new_records = []
#latest_currency_data.each do |key, value|
if existing_data.values.include? key
id = existing_data.find{|k,v| v.include? key}[0]
updated_data[id] = value
else
new_records << Page.new(value)
end
end
If anyone has a better way, it'd be gladly appreciated. Thanks!
Hi i had created a small ruby project which consists of JSON file. I stored the JSON data into hash keys. AND worte a method to access the data which is present in hash key using user input. But when i try to send use the user input i am getting this error
how_many_ingredients': undefined methodkeys' for nil:NilClass (NoMethodError)
I found this link with same question and tried that solution but still i'm getting the same error
Accessing Hash Keys with user inputted variables, NoMethodError
File one where all the methods are written
require 'json'
class Methods
attr_accessor :name, :text
def initilize(name)
#name = name
#text = text
end
def how_many_ingredients(text)
puts 'text'
file = File.read('a.json')
hash = JSON.parse(file)
#puts hash['recipes']['pizza'].keys
puts hash['recipes'][text].keys
end
end
File 2 where how_Many_ingredients method is accessed, I can see that the variable is passed to that method
require './1'
class Hello < Methods
person = Methods.new
person.test
puts "enter recipie"
person.name
str = gets
person.how_many_ingredients str
end
Note that when you use gets, the input can contain newline and carriage return characters. You'll need to use gets.chomp to filter these. This is likely the cause of the issue in your program.
Compare the following two:
> puts gets.size
"Hello!"
# 7
> puts gets.chomp.size
"Hello!"
# 6
Note that you'll still need to extend your program to account for user inputted keys that are not in your hash.
Your code assumes that there will always be a hash stored at hash['recipes'][text] - you need to cater to the cases where it isn't.
A simple way to do this is to work your way down through the hash with && symbols - if any step is nil (or false), the line will return nil (or false) rather than exploding. eg
puts hash['recipes'] && hash['recipes'][text].is_a?(Hash) && hash['recipes'][text].keys
Note i'm testing that hash['recipes'][text] is a hash (rather than just a string for example) before calling .keys on it.
In Grails, if I define a locale, and put a date on specific format on i18n file, like (dd/mm/AAAA), if call one request like:
http://myapp/myaction?object.date=10/12/2013
When I get print: params.date, it comes to me a date object.
How can I do the same on rails?
Normally the Rails handles this for you. For instance, the form helper datetime_select works in conjunction with some activerecord magic
to ensure ensure time/date types survive the round-trip. There are various alternatives to the standard date-pickers.
If this doesn't work for you e.g. rails isn't generating the forms, there are (at least) a couple of options.
One option, slightly evi, is to monkey-patch HashWithIndifferentAccess (used by request params) to do type conversions based on the key name. It could look something like:
module AddTypedKeys
def [](key)
key?(key) ? super : find_candidate(key.to_s)
end
private
# look for key with a type extension
def find_candidate(key)
keys.each do |k|
name, type = k.split('.', 2)
return typify_param(self[k], type) if name == key
end
nil
end
def typify_param(value, type)
case type
when 'date'
value.to_date rescue nil
else
value
end
end
end
HashWithIndifferentAccess.send(:include, AddTypedKeys)
This will extend params[] in the way you describe. To use it within rais, you can drop it into an initialiser, eg confg/initializers/typed_params.rb
To see it working, you can test with
params = HashWithIndifferentAccess.new({'a' => 'hello', 'b.date' => '10/1/2013', 'c.date' => 'bob'})
puts params['b.date'] # returns string
puts params['b'] # returns timestamp
puts params['a'] # returns string
puts params['c'] # nil (invalid date parsed)
However... I'm not sure it's worth the effort, and it will likely not work with Rails 4 / StrongParameters.
A better solution would be using virtual attributes in your models. See this SO post for a really good example using chronic.
token = params[:token]
email = params[:email]
phone_number = params[:phone_number]
iso_code = params[:iso_code]
if token.nil? or email.nil? or phone_number.nil? or iso_code.nil?
raise InvalidParameterException
end
token.strip!
email.strip!
phone_number.strip!
iso_code.strip!
#use the variables
There are many this code in my rails project.
How do you generalization this pattern in Ruby on Rails?
Is it possible with reflection and meta programming?
unless [:token, :email, :phone_number, :iso_code].-(params.keys).empty?
raise InvalidParameterException
end
params.each{|_, v| v.strip!}
Then, just use it like params[:token] each time. Maybe you can use a shorter variable name for params like p.
I don't think that metaprogramming or patterns are necessary in this case, just common sense:
[:token, :email, :phone_number, :iso_code].each do |k|
raise InvalidParameterException if params[k].nil?
params[k] = params[k].strip
end
I've never understood the popularity of this anti-pattern:
par_1 = params[:par_1]
...
par_n = params[:par_n]
Why not use the params[:par_x] instead? It's usually more convenient to work with params variables grouped in a hash than have them stored into bunch of local variables.
You can define the method below :
def verify_presence_define_var_and_strip(params, symbol_list)
symbol_list.each do |s|
raise InvalidParameterException if params[s].nil?
define_method s, params[s].strip
end
end
With this method, your code could be replace by :
verify_presence_define_var_and_strip(params, [:token, :email, :phone_number, :iso_code])
Notice that it will define method and not just set a local variable but you should have the same result if you do not already have a method with this name in your class.
You may prefer to use the instance_variable_set method but you will have to preceed the variable name by an #.
[UPDATE]
If you really want to define local variable and not method / instance variable, I do not know other solution than using eval :
eval "#{s} = #{params[:s].strip}"
But as you will find if you search about eval, it is considered as bad practice, moreover in this case where you will evaluate values from URL / POST parameters !
Create a helper method that access the params hash and iterates through the elements and checks for nil and throws the exception itself or returns a value to the calling method that could throw the exception.
You can iterate any hash like so,
hash.each do |key,value|
#example nil check
if value.nil?
#do what you want
end
end
If you want to raise on nil in your hash of parameters you could do
raise InvalidParameterException if params.values.include? nil
If you only want to raise on some specific parameters you would have first to pick the values associated to those keys:
required_keys = [:key1, :key2, :key3]
raise invalidParameterException if required_keys.map{|k| params[k]}.include? nil
EDIT: Cannot answer because of lack of points but one of the answer does not work I believe:
[:some_key] - {some_key: nil}.keys # => []
If the parameter hash contains one key initialized with value nil (as a result of a failed parse or invalid user input), Array subtraction does not work.