Elegantly handling blank values in a nested hash [duplicate] - ruby-on-rails

This question already has answers here:
How to avoid NoMethodError for missing elements in nested hashes, without repeated nil checks?
(16 answers)
Closed 7 years ago.
I'm sure I've seen an elegant solution to this before, but I can't quite find it:
I have a rails controller which may-or-may-not have the following hash element:
myhash[:parent_field]
Inside that parent field, a child element could also be blank. I'm currently checking that via the (very ugly) method:
if (!myhash[:parent_field] || !myhash[:parent_field][:child_field] || myhash[:parent_field][:child_field].blank?)
Which works, but I figure - surely - there has to be a more elegant way. Just to reiterate:
myhash[:parent_field] may or may not exist
If it does exist, myhash[:parent_field][:child_field] may or may not exist
If that exists, it may or may not be blank.

#fetch is your friend:
my_hash.fetch(:parent, {})[:child].blank?

What I would do is just use local variables to ease your burden:
unless (p=foo[:parent]) && (c=p[:child]) && !c.blank?
# Stuff is borked!
end
But let's explore alternatives, for fun…
If you can't change your data structure (that's a Hash, by the way, not an Array) then you can use the Ruby andand gem in conjunction with the Rails' try as lazy ways of calling methods on things that might be nil objects.
You could alternatively change your data structure to Hashes that return empty auto-vivifying hashes when you ask for a key that does not exist:
mine = Hash.new{ |h,k| Hash.new(&h.default_proc) }
mine[:root] = { dive:42 }
p mine[:root][:dive] #=> 42
p mine[:ohno][:blah][:whee] #=> {}
p mine[:root][:blah][:whee] #=> undefined method `[]' for nil:NilClass (NoMethodError)
However, you'd have to ensure that every object in your hierarchy was one of these hashes (which I explicitly failed to do for the contents of :dive, resulting in the error).
For alternative fun, you could add your own magic lookup method:
class Hash
def divedive(*keys)
obj = self
keys.each do |key|
return obj unless obj && obj.respond_to?(:[])
obj = obj[key]
end
obj
end
end
if myarray.divedive(:parent,:child).blank?
# ...

This is a frequently asked question and should probably be closed as a duplicate of
Ruby - Access multidimensional hash and avoid access nil object
Is there a clean way to avoid calling a method on nil in a nested params hash?
Ruby: Nils in an IF statement
The first in that list was closed as a dup of the other two, though I believe my answer there has more comprehensive coverage of techniques to address this problem than the later two.

It probably depends on what your actual needs are, but an OOP approach to this would be to convert the arrays and hashes into actual objects. Each object would manage its relationships (similar to ActiveRecord), and would know how to get children and parents.

Since nil.blank? is true, you can remove the middle condition and simplify to this:
if !myarray[:parent_field] || myarray[:parent_field][:child_field].blank?
Also, calling a Hash myarray is a bit misleading.

Related

What is map(&:id) in Rails?

My question is not an error, it is for understanding. As I'm new to Rails, I can't read all the code yet.
what does (&:id) do after .map
#user_cnae_classifications = user.cnae_classifications.map(&:id)
what is the difference of .map with it and without it?
in this method call:
UserCnaeClassification.create(
user: #user,
cnae_classification_id: id
)
How do I read that part of the code...
user: #user,
cnae_classification_id: id
are they keys and values?
1 )
You should read some tutorials on map to get acquainted.
https://www.rubyguides.com/2018/10/ruby-map-method
But the short answer is that running user.cnae_classifications.map(&:id) will loop over all cnae_classifications and extract the id from them and put them all into an array. The & character allows for map shorthand to avoid passing an entire block.
From the link above:
2 )
The #create method can accept a key-value hash of known attributes (known to the class in question, in this case that is UserCnaeClassification) to assign upon creation. So you're basically right, they are key-value pairs but they are specific to this class/object. Those same keys might not work on another class/object.
Additional reading: https://guides.rubyonrails.org/active_record_basics.html#create
what does (&:id) do after .map
The syntax map(&:method) is equivalent to:
object.map do |i|
i.method
end
The complete explanation is that the & operator is used to convert any Ruby object that responds to to_proc into a Proc, which encapsulates a block of code. In this case, the Symbol object (:id) is converted into the block of code above.
If you're interested in learning more about it, notice this is pure Ruby, not Rails-specific. Check the documentation for Proc.
In this method call:
How do I read that part of the code...
are they keys and values?
These are keyword arguments. It's a way to name the parameters of a method to explicitly tell the reader what each value should be. Just be aware that the behavior of methods accepting hashes as keyword arguments is deprecated, as seen in this official post.
The .map(&:id) is a shorthand for the longer form of .map { |x| x.id }.
Some interesting things to say: if you're using database (ORM - ActiveRecord), you will see that writing map(&:id) could be helpful. There also exists method called pluck, which does similiar things, but it's a little faster.
Usage:
Also pluck doesn't work with regular Arrays.

Rails sort_by on Array throwing ArgumentError because of nil values - shouldn't it just sort depending on database?

I am sorting an Array in Rails 4 using sort_by:
sort_column = 'experience_in_months'
#userstoshow=#userstoshow.sort_by(&:"#{sort_column}")
This throws an error
An ArgumentError occurred in SNIP:
comparison of Fixnum with nil failed
This is a strange problem because I remember reading about this earlier. It was supposed to put the nil values first or last based on the database used. I am using PostGreSQL.
I remember this working quite flawlessly a few days ago - it just put nil values at the top in my table.
It isn't a difficult problem to solve - I can take out the nil values, sort the rest and then add back the nil enteries. But I would like to be clear if this is a random behaviour or if it is as expected.
#arunt, I am guessing that you have in #userstoshow objects of type User or something as such.
Now when you use Ruby's sort_by it enumerates over the collection and tries to a <=> b, here is an equivalent code snippet.
#userstoshow = #userstoshow.sort_by do |a, b|
a.experience_in_months <=> b.experience_in_months
end
Now Ruby doesn't know how to compare a Fixnum to a NilClass, here is a similar case
[1, 2, 3, nil].sort => ArgumentError: comparison of Fixnum with nil failed
So the problem is that your User object is returning nil for some of it's object rather than a Fixnum value.
So you can either change your sort_by to include a code block or fix your User object to return a default value which is not nil.
This is expected behavior
Database ordering only applies in database queries
Once the data is out of the database, Ruby has full control and ruling over ordering rules. Databases' rules no longer apply because arrays are used everywhere and no gems expect them to be broken in this way.
Taking advantage of the fact that nil.to_i is 0, you can use the following:
attribute_proc = sort_column.to_sym.to_proc
#userstoshow = #userstoshow.sort_by { |x| attribute_proc.call(x).to_i }
...and you're replacing the value in-place, so you can use sort_by!, not sort_by:
attribute_proc = sort_column.to_sym.to_proc
#userstoshow.sort_by! { |x| attribute_proc.call(x).to_i }
Looks kinda intimidating. That's because you're not using a fixed attribute name. Otherwise, it'd be:
#userstoshow.sort_by! { |x| x.experience_in_months.to_i }

How to save nil into serialized attribute in Rails 4.2

I am upgrading an app to Rails 4.2 and am running into an issue where nil values in a field that is serialized as an Array are getting interpreted as an empty array. Is there a way to get Rails 4.2 to differentiate between nil and an empty array for a serialized-as-Array attribute?
Top level problem demonstration:
#[old_app]
> Rails.version
=> "3.0.3"
> a = AsrProperty.new; a.save; a.keeps
=> nil
#[new_app]
> Rails.version
=> "4.2.3"
> a = AsrProperty.new; a.save; a.keeps
=> []
But it is important for my code to distinguish between nil and [], so this is a problem.
The model:
class AsrProperty < ActiveRecord::Base
serialize :keeps, Array
#[...]
end
I think the issue lies with Rails deciding to take a shortcut for attributes that are serialized as a specific type (e.g. Array) by storing the empty instance of that type as nil in the database. This can be seen by looking at the SQL statement executed in each app:
[old_app]: INSERT INTO asr_properties (lock_version, keeps)
VALUES (0, NULL)
Note that the above log line has been edited for clarity; there are other serialized attributes that were being written due to old Rails' behavior.
[new_app]: INSERT INTO asr_properties (lock_version)
VALUES (0)
There is a workaround: by removing the "Array" declaration on the serialization, Rails is forced to save [] and {} differently:
class AsrProperty < ActiveRecord::Base
serialize :keeps #NOT ARRAY
#[...]
end
Changing the statement generated on saving [] to be:
INSERT INTO asr_properties (keeps, lock_version) VALUES ('---[]\n', 0)
Allowing:
> a = AsrProperty.new; a.save; a.keeps
=> nil
I'll use this workaround for now, but:
(1) I feel like declaring a type might allow more efficiency, and also prevents bugs by explicitly prohibiting the wrong data type being stored
(2) I'd really like to figure out the "right" way to do it, if Rails does allow it.
So: can Rails 4.2 be told to store [] as its own thing in a serialized-as-Array attribute?
What's going on?
What you're experiencing is due to how Rails 4 treats the 2nd argument to the serialize call. It changes its behavior based on the three different values the argument can have (more on this in the solution). The last branch here is the one we're interested in as when you pass the Array class, it gets passed to the ActiveRecord::Coders::YAMLColumn instance that is created. The load method receives the YAML from the database and attempts to turn it back into a Ruby object here. If the coder was not given the default class of Object and the yaml argument is nil in the case of a null column, it will return a new instance of the class, hence the empty array.
Solution
There doesn't appear to be a simple Rails-y way to say, "hey, if this is null in the database, give me nil." However looking at the second branch here we see that we can pass any object that implements the load and dump methods or what I call the basic coder protocol.
Example code
One of the members of my team built this simple class to handle just this case.
class NullableSerializer < ActiveRecord::Coders::YAMLColumn
def load(yaml)
return nil if yaml.nil?
super
end
end
This class inherits from the same YAMLColumn class provided by ActiveRecord so it already handles the load and dump methods. We do not need any modifications to dump but we want to slightly handle loading differently. We simply tell it to return nil when the database column is empty and otherwise call super to work as if we made no other modification.
Usage
To use it, it simply needs to be instantiated with your intended serialization class and passed to the Rails serialize method as in the following, using your naming from above:
class AsrProperty < ActiveRecord::Base
serialize :keeps, NullableSerializer.new(Array)
# …
end
The "right" way
Getting things done and getting your code shipped is paramount and I hope this helps you. After all, if the code isn't being used and doing good, who cares how ideal it is?
I would argue that Rails' approach is the right way in this case especially when you take Ruby's philosophy of The Principle of Least Surprise into account. When an attribute can possibly be an array, it should always return that type, even if empty, to avoid having to constantly special case nil. I would argue the same for any database column that you can put a reasonable default on (i.e. t.integer :anything_besides_a_foreign_key, default: 0). I've always been grateful to past-Aaron for remembering this most of the time whenever I get an unexpected NoMethodError: undefined method 'whatever' for nil:NilClass. Almost always my special case for this nil is to supply a sensible default.
This varies greatly on you, your team, your app, and your application and it's needs so it's never hard and fast. It's just something I've found helps me out immensely when I'm working on something and wondering if amount could default to 0 or if there's some reason buried in the code or in the minds of your teammates why it needs to be able to be nil.

Remove element from ActiveRecord_Relation in rails

How can i remove the last element from an ActiveRecord_Relation in rails?
e.g. if I set:
#drivers = Driver.all
I can add a another Driver object called #new_driver to #drivers by doing:
#drivers << #new_driver
But how can I remove an object from #drivers?
The delete method doesn't seem to work, i.e.
#drivers.delete(0)
You can use the reject! method, this will remove the object from the collection without affecting the db
for example:
driver_to_delete = #driver.first # you need the object that you want removed
#drivers.reject!{|driver| driver == driver_to_delete}
Very late too, but I arrived here looking for a fast answer and finished by thinking by myself ;)
Just to clarify about the different answers and the Rails 6.1 comment on accepted answer:
The OP wanted to remove one entry from a query, but NOT remove it from database, so any answer with delete or destroy is just wrong (this WILL delete data from your database !!).
In Ruby (and therefore Rails) convention, shebang methods (ending with !) tend to alter the given parameter. So reject! would imply modifying the source list ... but an ActiveRecord_Relation is basically just a query, NOT an array of entries !
So you'd have 2 options:
Write your query differently to specifically say you don't want some id:
#drivers.where.not(id: #driver_to_remove) # This still is an ActiveRecord_Relation
Use reject (NO shebang) on your query to transform it into an Array and "manually" remove the entry you don't want:
#drivers.reject{ |driver| driver == #driver_to_remove}
# The `reject` forces the execution of the query in DB and returns an Array)
On a performance point of view, I would personally recommend the first solution as it would be just a little more complex against the DB where the latter implies looping on the whole (eventually large) array.
Late to the question, but just had the same issue and hope this helps someone else.
reject!did not work for ActiveRecord_Relation in Rails 4.2
drop(1) was the solution
In this case #drivers.drop(0) would work to drop the first element of the relation
Since its an array of objects, have you tried to write something like #drivers.delete(#new_driver) or #drivers.delete(id: #new_driver.id) ?
This is the documentation you need:
#group.avatars << Avatar.new
#group.avatars.delete(#group.avatars.last)
--
.destroy
The problem you've got is you're trying to use collection methods on a non-collection object. You'll need to use the .destroy ActiveRecord method to get rid of the record from the database (and consequently the collection):
#drivers = Driver.all
#drivers.last.destroy
--
Scope
.delete will remove the record from the DB
If you want to pull specific elements from the db to populate the #drivers object, you'll need to use a scope:
#app/models/driver.rb
Class Driver < ActiveRecord::Base
scope :your_scope, -> { where column: "value" }
end
This will allow you to call:
#app/controllers/drivers_controller.rb
def index
#drivers = Driver.your_scope
end
I think you're getting the MVC programming pattern confused - data manipulation is meant to happen in the model, not the controller
As stated above, reject! doesn't work in Rails 4.2, but delete does, so #drivers.delete(#new_driver) works, and more generally:
#drivers.delete(Driver.where(your condition))

Why am I getting this 'can't modify frozen hash' error?

I have a Person model & an Item model. A person has many items, and an item belongs to a person.
In this code, I need to delete the existing items for a person, and create new ones from a parameter (which is an array of hashes). Then, I need to update one of the item's fields, based on one of its other fields.
#person = Person.find(params["id"])
#person.person_items.each do |q|
q.destroy
end
person_items_from_param = ActiveSupport::JSON.decode(params["person_items"])
person_items_from_param.each do |pi|
#person.person_items.create(pi) if pi.is_a?(Hash)
end
#person.person_items.each do |x|
if x.item_type == "Type1"
x.item_amount = "5"
elsif x.item_type == "Type2"
x.item_amount = "10"
end
x.save
end
On the x.item_amount = "5" & x.item_amount = "10" lines I get this error:
RuntimeError in PersonsController#submit_items
can't modify frozen hash
How can I fix this? Thanks for reading.
I would suspect
ActiveSupport::JSON.decode(params["person_items"])
returns a frozen hash which you then use to create objects
#person.person_items.create(pi) if pi.is_a?(Hash)
And since its frozen you can't modify it.
You could
A
Make a deep copy of the JSON object
or
B
Reload the model instance which should reinstantiate the object making the fields unfrozen.
Option A is the "better" solution but difficult because the only way I know of deep copying is serializing and deserializing and object in place and assigning the return value.
If you use q.destroy before saving element then you will get the error. better save the element first and then use destroy.
You can get around this if you read the person_items from the database again rather than using the association. The association is stale and pointing to the destroyed rows.
Instead of
#person.person_items.each do |x|
Try
PersonItem.where(:person_id=>#person.id).each do |x|
You can make a deep copy of any object in rails includes JSON, so just do it.
Remember that clone preserves the frozen state, while dup doesn't.
Easiest way to fix error can't modify frozen Array is to dup this frozen array ;)
person_items_from_param = ActiveSupport::JSON.decode(params["person_items"]).dup

Resources