I want to serialize any object(s) in text columns.
Via an API, I get the params:
params[:attachments] -- this can be 0 or 1 or 3, or 100+ etc...
params[:attachment1]...params[:attachment2] ... params[:attachmentN]
So how do I store X # of attachments in a serialized object?
mailThing = MailThing.create(:attachments => myAttachmentsSerizliedIfANY )
I'm trying to do:
#myAttachmentsSerizliedIfANY = nil
i = 0
attachmentCount = params[:attachments].to_i
while i < attachmentCount do
#myAttachmentsSerizliedIfANY << params[:attachment + i ]
i += 1
end
Any suggestions on how to get this working? thanks
Okay, so I looked at some of your other questions, and I think I might have something that will work for you. For this to work, you will need to have a column in the database (I'll call it attachment_storage) where you can store these attachments after you serialize them.
Basically you want to get the attachments into an array first, and then serialize it into a string so that you can store it into the database.
Here's some code to get that done.
attachment_storage = []
(1..params[:attachments].to_i).each do |attachment_num|
attachment_storage << params["attachment#{attachment_num}".to_sym]
end
Here we're building the symbols for the params hash using the string and to_sym to turn it into a symbol like :attachment1, :attachment2, etc.
Then you want to put in the database, so you can store it as noted in the [ActiveRecord Documentation][1] under the section "Saving arrays, hashes, and other non-mappable objects in text columns".
In order for the serialization to work, you need to add serialize :attachment_storage to your model, and then to store it you would assign it just like any other parameter as above. Then save your model and it will be serialized for you.
Related
I have a user model with a friends column of type text. This migration was ran to use the array feature with postgres:
add_column :users, :friends, :text, array: true
The user model has this method:
def add_friend(target)
#target would be a value like "1234"
self.friends = [] if self.friends == nil
update_attributes friends: self.friends.push(target)
end
The following spec passes until I add user.reload after calling #add_friend:
it "adds a friend to the list of friends" do
user = create(:user, friends: ["123","456"])
stranger = create(:user, uid: "789")
user.add_friend(stranger.uid)
user.reload #turns the spec red
user.friends.should include("789")
user.friends.should include("123")
end
This happens in development as well. The model instance is updated and has the new uid in the array, but once reloaded or reloading the user in a different action, it reverts to what it was before the add_friend method was called.
Using Rails 4.0.0.rc2 and pg 0.15.1
What could this be?
I suspect that ActiveRecord isn't noticing that your friends array has changed because, well, the underlying array reference doesn't change when you:
self.friends.push(target)
That will alter the contents of the array but the array itself will still be the same array. I know that this problem crops up with the postgres_ext gem in Rails3 and given this issue:
String attribute isn't marked as dirty, when it changes with <<
I'd expect Rails4 to behave the same way.
The solution would be to create a new array rather than trying to modify the array in-place:
update_attributes friends: self.friends + [ target ]
There are lots of ways to create a new array while adding an element to an existing array, use whichever one you like.
It looks like the issue might be your use of push, which modifies the array in place.
I can't find a more primary source atm but this post says:
One important thing to note when interacting with array (or other mutable values) on a model. ActiveRecord does not currently track "destructive", or in place changes. These include array pushing and poping, advance-ing DateTime objects. If you want to use a "destructive" update, you must call <attribute>_will_change! to let ActiveRecord know you changed that value.
If you want to use Postgresql array type, you'll have to comply with its format. From Postgresql docs the input format is
'{10000, 10000, 10000, 10000}'
which is not what friends.to_s will return. In ruby:
[1,2,3].to_s => "[1,2,3]"
That is, brackets instead of braces. You'll have to do the conversion yourself.
However I'd much rather rely on ActiveRecord serialize (see serialize). The database does not need to know that the value is actually an array, that's your domain model leaking into your database. Let Rails do its thing and encapsulate that information; it already knows how to serialize/deserialize the value.
Note: This response is applicable to Rails 3, not 4. I'll leave here in case it helps someone in the future.
I am connecting to an API and I get array of hashes or only 1 hash for the data. So when the data comes as array of hashes;
"extras"=>{"extra"=>[{"id"=>"529216700000100800", "name"=>"Transfer Trogir - Dubrovnik (8 persons max)", "price"=>"290.0", "currency"=>"EUR", "timeunit"=>"0", "customquantity"=>"0", "validdatefrom"=>"1970-01-01", "validdateto"=>"2119-07-20", "sailingdatefrom"=>"1970-01-01", "sailingdateto"=>"2119-07-20", "obligatory"=>"0", "perperson"=>"0", "includedinbaseprice"=>"0", "payableoninvoice"=>"1", "availableinbase"=>"-1", "includesdepositwaiver"=>"0", "includedoptions"=>""}, {"id"=>"528978430000100800", "name"=>"Gennaker + extra deposit (HR)", "price"=>"150.0", "currency"=>"EUR", "timeunit"=>"604800000", "customquantity"=>"0", "validdatefrom"=>"1970-01-01", "validdateto"=>"2119-07-19", "sailingdatefrom"=>"1970-01-01", "sailingdateto"=>"2119-07-19", "obligatory"=>"0", "perperson"=>"0", "includedinbaseprice"=>"0", "payableoninvoice"=>"1", "availableinbase"=>"-1", "includesdepositwaiver"=>"0", "includedoptions"=>""}]
I'am looping through the array to get the values as;
b["extras"]["extra"].each do |extra|
puts extra["id"]
puts extra["name"]
end
But when this is not array; only 1 hash, then this is not working, adding each loop makes it array but not array of hashes;
"extras"=>{"extra"=>{"id"=>"640079840000100800", "name"=>"Comfort package (GRE)", "price"=>"235.0", "currency"=>"EUR", "timeunit"=>"0", "customquantity"=>"0", "validdatefrom"=>"1970-01-01", "validdateto"=>"2120-03-25", "sailingdatefrom"=>"2015-01-01", "sailingdateto"=>"2120-03-25", "obligatory"=>"1", "perperson"=>"0", "includedinbaseprice"=>"0", "payableoninvoice"=>"1", "availableinbase"=>"-1", "includesdepositwaiver"=>"0", "includedoptions"=>""}}
b["extras"]["extra"].each do |extra|
puts extra["id"]
puts extra["name"]
end
This time, that gives error TypeError (no implicit conversion of String into Integer);
When I type puts extra.inspect; I get ["id", "640079840000100800"]. So to make it work I should pass extra[1] to get the id number.
But I can not predict either array of hashes or only hash. Is there any easy way to solve this issue that works either array of hashes or just a hash?
Naïve solution: one might check the object’s type upfront:
case b["extras"]["extra"]
when Array
# handle array
when Hash
# handle hash
end
Proper solution: produce an array of hashes no matter what came.
[*[input]].flatten
and deal with it as with an array having at least one hash element (with each.)
Please also refer to valuable comment by #Stefan below if you have no allergy using Rails helpers.
You could try to use Object#kind_of? to determine whether it is an Array or a Hash instance.
if b["extras"]["extra"].kind_of? Array
# handle array
elsif b["extras"]["extra"].kind_of? Hash
# handle hash
end
I have parameters from an external API that formats JSON responses using CamelCase that I want to fit into my Rails app:
{"AccountID"=>"REAWLLY_LONG_HASH_API_KEY_FROM_EXTERNAL_SERVICE",
"ChannelProductDescription"=>"0004", "Currency"=>"CAD",
"CurrentBalance"=> {"Amount"=>"162563.64", "Currency"=>"CAD"}}
Using the below script I converted them to lower case:
data = JSON.parse(response, symbolize_keys: true)
data = {:_json => data} unless data.is_a?(Hash)
data.deep_transform_keys!(&:underscore)
data.deep_symbolize_keys!
Leaving me correctly formatted params like this:
{:account_id=>"REAWLLY_LONG_HASH_API_KEY_FROM_EXTERNAL_SERVICE",
:channel_product_description=>"SAVINGS", :currency=>"CAD",
:current_balance=> {:amount=>"43.00", :currency=>"CAD"}}
I'm trying to map this external API response into a generic Rails model Account, where JSON from this API call will return cleanly as parameters into my database to allow a clean saving interface such as:
#account = Account.create(ParamParser.call(params))
But I ran into a problem with converting :account_id, as that param conflicts with the primary key of my database.
To get around this, my idea is to convert all symbol instances of params[:account_id] into params[:account_key_id], so that those params don't conflict with my databases existing account_id field.
How do I do this, and is there a better approach for consuming external JSON API's than what I've described here?
Hash#deep_transform_keys does this:
Returns a new hash with all keys converted by the block operation.
This includes the keys from the root hash and from all
nested hashes and arrays.
So you could do it in one pass with an appropriate block, something like:
data.deep_transform_keys! do |key|
key = key.underscore.to_sym
key = :account_key_id if(key == :account_id)
key
end
You might as well drop the symbolize_keys: true flag to JSON.parse too, you're changing all the keys anyway so don't bother.
If you're doing this sort of thing a lot then you could write a method that takes a key mapping Hash and gives you a lambda for transforming the keys:
def key_mangler(key_map = { })
->(key) do
key = key.underscore.to_sym
key = key_map[key] if(key_map.has_key?(key))
key
end
end
and then say things like:
data.deep_transform_keys!(&key_mangler(:account_id => :account_key_id))
You might want to use a different name than key_mangler of course but that name is good enough to illustrate the idea.
BTW, if you're sending this JSON into the database then you probably don't need to bother with symbol keys, JSON only uses strings for keys so you'll be converting strings to symbols only for them to be converted back to strings. Of course, if you're symbolizing the keys when pulling the JSON out of the database then you'll probably want to be consistent and use symbols across the board.
In addition to the previous answer...
Unfortunately, there is, to my knowledge, no method on Hash that does this in one operation. I've always accomplished this by brute force, as in:
hash[:new_key] = hash[:old_key]
hash.delete(:old_key)
A shortcut for this, suggested in the comment below by "mu is too short", is:
hash[:new_key] = hash.delete(:old_key)
To illustrate in irb:
2.4.1 :002 > h = { foo: :bar }
=> {:foo=>:bar}
2.4.1 :003 > h[:foo_new] = h.delete(:foo)
=> :bar
2.4.1 :004 > h
=> {:foo_new=>:bar}
Let's say I have some active resource object fetched as the following:
x = Resource.find(some_id)
And x in the remote server has some field h as a complex nested hashes which is represented here as nested active resource objects, but then accessing it is a tedious task, so is it possible to convert h to hash? I can just make another call Resource.get(some_id) and the result will be one big hash, but this is risky as the resource -theoretically- may have changed between subsequent calls, so is there a way to convert active resource object to hash ?
Edit
For more clarification, Suppose that some invoice record r[id=some_id] has attribute extras, which is a hash with the value: {:x=>1, :y=>2, :z=>{:a=>1, :b=>2}}
Then when fetching this record through active resource, we get the following result for extras field, -extracted from the response-:
"extras"=>
#<App::Invoice::Extras:0x00000008202cb0
#attributes=
{"x"=>1,
"y"=>2,
"z"=>
#<App::Invoice::Extras::Z:0x00000008201978
#attributes={"a"=>1, "b"=>2},
#persisted=true,
#prefix_options={}>},
#persisted=true,
#prefix_options={}>,
Then how to convert this extras field to a ruby hash ?
JSON.parse(x.to_json) did the trick.
Try:
x = Resource.find(some_id)
hash = OpenStruct.new(x).to_h
or
hash = OpenStruct.new(x.attributes).to_h
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