Im creating a simple rails app to modify data in an existing mongo database. I'm using mongoid for the interaction and can read/destroy objects just fine.
The problem comes is my mongo document has a 'node' which is a bunch of key value pairs with vary depending on the record. When i load the record like so:
MongoObject.find(BSON::ObjectId('ABC1234567890'))
=> #<MongoObject _id: ABC1234567890, node: {"totallogins"=>11, "id"=>"logIns"}>
I'm using a standard rails form to update the values so the post data looks like:
{"commit"=>"Edit", "utf8"=>"✓", "id"=>"ABC1234567890", "mongo_object"=>{"node"=>{"totallogins"=>"12", "id"=>"logIns"}}
If i then do:
#mongo_object.update_attributes(params[:mongo_object])
This works but changes the datatype of "totallogins" from an int to a string because the post data is a string.
Now active record deals with this itself but i need a solution that will work with mongoid.
Any ideas how i can do this?
Thanks. Unfortunately i can't as the fields for node are totally dynamic so i can't define them. I've come up with the following solution but its a tad ugly:
#mongo_object.node.each do |k,v|
new_value = params[:mongo_object][:node][k.to_sym]
new_value = new_value.to_i if v.class == Fixnum
#mongo_object.node[k] = new_value
end
#mongo_object.save
If you make the node an embedded_document, then you can explicitly set the field types when you declare them.
class Node
include Mongoid::Document
embedded_in :mongo_object
field :totallogins, type: Integer
...
end
http://mongoid.org/docs/documents/ mentions how to deal with types; perhaps make sure your type is an Integer?
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'm using Rails4's Store feature. I've added a new stored attributes namely "friends" with four accessors, first ... fourth.
The problem is how to utilize it in the "where" condition. When I use it as:
#persons = Person.where(friends.has_value?#user.id)
I receive this error:
NameError in UsersController#myfrineds
undefined local variable or method `friends'
I tried some other different ways but still I get error. Could you please help me to solve it? Or please let me know if you have any better idea to implement it (storing a dynamic hash of key/values)?
As stated by Uzbekjon, store is not made for this kind of things. Some workarounds to your problem:
Use a custom query (would be quite slow depending on table size so be careful):
#persons = Person.where('friends LIKE ? OR friends LIKE ? OR friends LIKE ? OR friends LIKE ?', "%first: #{#person.id}\n%", "%second: #{#person.id}\n%", "%third: #{#person.id}\n%", "%fourth: #{#person.id}\n%")
This assumes you used yaml for the serialization of friends attribute (it's the default). If you used json you'll have to change the query accordingly.
If you're using PostgreSQL you can use array attribute instead of store. Queries would have better timings since PostgreSQL supports this datatype.
Migration:
def change
add_column :people, :friends, :text, array: true, default: []
# if you want to add index:
add_index :people, :friends, using: 'gin'
end
Creation of records:
Person.create(..., friends: [friend_id_1, friend_id_2, friend_id_3, friend_id_4])
Query:
#persons = Person.where('? = ANY(friends)', #person.id)
You may also need to add to your Gemfile:
gem 'postgres_ext'
Hope it helps!
Short answer - You can't! Because, ActiveRecord stores your "hash" as a string in a single column. The only way I can think of is to use .where("friends LIKE :friend", friend: 'BFF') syntax. Don't forget to index your column though.
It is mentioned in the docs as well:
It's like a simple key/value store baked into your record when you don't care about being able to query that store outside the context of a single record.
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'm building an activerecord to model a conversation tree, using an array column type to represent the materialized path of the record's place in that tree, using postgres 9.1, rails 4.0, and the pg gem.
What I really want to do is access currval('conversations_id_seq') when I create a new conversation object, so that I can pass in [grandparent_id, parent_id ... current_id] as the array to the object initializer. That way I can specify that this column is not null as a database constraint, and in the event of a parentless conversation, have it still default to [current_id].
The problem I have is getting access to the model's id value before I save it the first time. I could always relax the not null constraint and add an after_create hook, but that feels kludgy. I'm hopeful that there's a way I can grab the value that's getting pushed into #id inside the initializer, before the first save to the database.
EDIT to clarify for the bounty: In an ideal world, there would be a special token I could pass in to the object's create method: Conversation.create(reply_chain: [:lastval]), where the gem took that to mean lastval() in the generated SQL.
something like:
def before_create
self.id=Conversation.connection.execute("SELECT nextval('conversations_id_seq')")
self.path = [... , self.id];
true
end
or use a before insert/update trigger to maintain the path.
You could alias the attribute if you don't need the column in the database.
alias_attribute :current_id, :id
Or you could query for the id when you need it.
def self.last_val
ActiveRecord::Base.connection.execute("SELECT lastval('conversations_id_seq')")
end
def self.next_val
ActiveRecord::Base.connection.execute("SELECT nextval('conversations_id_seq')")
end
Conversation.create(reply_chain: Conversation.next_val)
Using after_save isn't the ugliest of code either.
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.