Mongoid - Array management ? insert unique value, remove value if exists? - ruby-on-rails

I am trying to do something rather simple I believe:
1) insert a value in an array field only if that value is not already present
2) remove a value if it exists in the array
I have just no idea how to do any of this things... for the moment I am just inserting my value without checking if it exists already: myArray << obj.id
Thanks,
Alex
ps: using Rails 3.0.3, mongo 1.1.5 and mongoid 2.0.0.rc5
ps2: this is the mongodb syntax to achieve what I want, but I have no idea how to do this in mongoid
{ $addToSet : { field : value } }
Adds value to the array only if its not in the array already, if field is an existing array, otherwise sets field to the array value if field is not present. If field is present but is not an array, an error condition is raised.
To add many valuest.update
{ $addToSet : { a : { $each : [ 3 , 5 , 6 ] } } }
$pop
{ $pop : { field : 1 } }
removes the last element in an array (ADDED in 1.1)
{ $pop : { field : -1 } }
removes the first element in an array (ADDED in 1.1) |

You want to use the add_to_set method, as documented (somewhat) here: http://mongoid.org/en/mongoid/docs/persistence.html#atomic
Example:
model = Model.new
model.add_to_set(:field, value)
model.save
You can give it a single value or even an array of values. The latter will use mongo's $each qualifier along with $addToSet when adding each element of your array to the field you specify.

As per Chris Hawk from Mongoid googlegroup:
Arrays in Mongoid documents are simple Ruby arrays. See the docs for
the Array class: http://www.ruby-doc.org/core/classes/Array.html
So, for insertion you can simply do:
array << object unless array.include?(object)
And for removal:
array.delete(object)

worth mentioning, in mongoid, as of 2.0.0pre4 I do not see any addToSet support. mongo_mapper (while imo less maintained :( ) supports this via push_uniq method.
short of that, in mongoid, if you are working with the relationship method directly, you don't need to do the include?. if you are working with the array directly, you do.
Example:
class Person
include Mongoid::Document
has_and_belongs_to_many :pets ## or has_many :pets, :stored_as => :array if your not using the latest mongoid
end
#when using .pets no need to check, mongoid checks for you to ensure you can only add the same pet once.
pet = Pet.find("294x29s9a9292")
p = Person.find("42192189a92191a")
p.pets << pet
#when using pet_ids, you do need to check:
pet = Pet.find("294x29s9a9292")
p = Person.find("42192189a92191a")
p.pet_ids << pet.id unless p.pet_ids.include? pet.id

You can use the pull operator, which is atomic, to remove a single element. Here's an example, where l is a reference to a document with an array field array:
l.array = [1, 2, 3]
l.save
l.pull(array: 1) # atomically removes 1, leaving [2, 3]
Adding can be done with add_to_set, as previously mentioned:
l.array = [2, 3]
l.save
l.add_to_set(array: 1) # atomically adds 1, resulting in [2, 3, 1]
Here's a link to the current Mongoid documentation on Atomic operators.

Related

Storing Postgres Array of Jsonb in Rails 5 Escapes Strings Unexpectedly

Perhaps my understanding of how this is supposed to work is wrong, but I seeing strings stored in my DB when I would expect them to be a jsonb array. Here is how I have things setup:
Migration
t.jsonb :variables, array: true
Model
attribute :variables, :variable, array: true
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable, Variable::Type)
Custom Variable Type
class Variable::Type < ActiveRecord::Type::Json
include ActiveModel::Type::Helpers::Mutable
# Type casts a value from user input (e.g. from a setter). This value may be a string from the form builder, or a ruby object passed to a setter. There is currently no way to differentiate between which source it came from.
# - value: The raw input, as provided to the attribute setter.
def cast(value)
unless value.nil?
value = Variable.new(value) if !value.kind_of?(Variable)
value
end
end
# Converts a value from database input to the appropriate ruby type. The return value of this method will be returned from ActiveRecord::AttributeMethods::Read#read_attribute. The default implementation just calls #cast.
# - value: The raw input, as provided from the database.
def deserialize(value)
unless value.nil?
value = super if value.kind_of?(String)
value = Variable.new(value) if value.kind_of?(Hash)
value
end
end
So this method does work from the application's perspective. I can set the value as variables = [Variable.new, Variable.new] and it correctly stores in the DB, and retrieves back as an array of [Variable, Variable].
What concerns me, and the root of this question, is that in the database, the variable is stored using double escaped strings rather than json objects:
{
"{\"token\": \"a\", \"value\": 1, \"default_value\": 1}",
"{\"token\": \"b\", \"value\": 2, \"default_value\": 2}"
}
I would expect them to be stored something more resembling a json object like this:
{
{"token": "a", "value": 1, "default_value": 1},
{"token": "b", "value": 2, "default_value": 2}
}
The reason for this is that, from my understanding, future querying on this column directly from the DB will be faster/easier if in a json format, rather than a string format. Querying through rails would remain unaffected.
How can I get my Postgres DB to store the array of jsonb properly through rails?
So it turns out that the Rails 5 attribute api is not perfect yet (and not well documented), and the Postgres array support was causing some problems, at least with the way I wanted to use it. I used the same approach to the problem for the solution, but rather than telling rails to use an array of my custom type, I am using a custom type array. Code speaks louder than words:
Migration
t.jsonb :variables, default: []
Model
attribute :variables, :variable_array, default: []
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable_array, VariableArrayType)
Custom Variable Type
class VariableArrayType < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
def deserialize(value)
value = super # turns raw json string into array of hashes
if value.kind_of? Array
value.map {|h| Variable.new(h)} # turns array of hashes into array of Variables
else
value
end
end
end
And now, as expected, the db entry is no longer stored as a string, but rather as searchable/indexable jsonb. The whole reason for this song and dance is that I can set the variables attribute using plain old ruby objects...
template.variables = [Variable.new(token: "a", default_value: 1), Variable.new(token: "b", default_value: 2)]
...then have it serialized as its jsonb representation in the DB...
[
{"token": "a", "default_value": 1},
{"token": "b", "default_value": 2}
]
...but more importantly, automatically deserialized and rehydrated back into the plain old ruby object, ready for me to interact with it.
Template.find(123).variables = [#<Variable:0x87654321 token: "a", default_value: 1>, #<Variable:0x12345678 token: "b", default_value: 2>]
Using the old serialize api causes a write with every save (intentionally by Rails architectural design), regardless of whether or not the serialized attribute had changed. Doing this all manually by overriding setters/getters is an unnecessary complication due to the numerous ways attributes can be assigned, and is partly the reason for the newer attributes api.
If it helps anyone else, Rails wants you to provide the possible keys to permit in the controller as well if you're using strong params:
def controller_params
params.require(:parent_key)
.permit(
jsonb_field: [:allowed_key1, :allowed_key2, :allowed_key3]
)
end
One solution could be to just parse the variable via JSON.parse, push it inside an empty array, then assign it to the attribute.
variables = []
variable = "{\"token\": \"a\", \"value\": 1, \"default_value\": 1}"
variable.class #String
parsed_variable = JSON.parse(variable) #{"token"=>"a", "value"=>1, "default_value"=>1}
parsed_variable.class #Hash
variables.push parsed_variable

How to compare objects in array RUBY

How to compare objects with the same parameter in ruby? How to define elsif part?
def compare
array_of_items = #items.map(&:object_id)
if array_of_items.uniq.size == array_of_items.size #array has only uniq vlaues - it's not possible to duplicate object - good!
return
elsif
#the comparision of objects with the same object_id by other param (i.e. date_of_lease param). The part I can not formulate
else
errors.add('It is not possible to purchase many times one item with the same values')
end
end
You can use Enumerable#group_by, e.g.
elsif #items.group_by(&:date_of_lease).count == array_of_items.size
As far as I understand, I guess you want to compare two objects with same object_id.
same_objects = #items.select { | element |
if array_of_items.count(element.object_id) > 1 do
# Duplicate object
end
}
I don't know how about other Ruby implementations, but in MRI Object#object_id returns unique (integer representation of the object in memory) value for every object. If you try to redefine it, you will get the warning:
class Object
def object_id
'a'
end
end
#=> warning: redefining `object_id' may cause serious problems
:object_id
First of all, since this is tagged as rails, isn't this the type of thing you can solve with a built in validation?
validates_uniqueness_of :date_of_lease, scope: :object_id
I don't know know about your implementation, but if you used the primary key of your database you might not even need that scope.
Otherwise, assuming you have overriden ruby object_id so two objects can have the same ids (¿?) I can only think of something complex like:
def compare
duplicate_items = #items.group_by(&:object_id).select { |k,v| v.size > 1}
if duplicate_items.keys.empty?
return
elsif duplicate_items.select{|k,v| v.group_by(&:date_of_lease).count != v.count}.empty?
# There are no duplicate object ids that also have duplicate
# dates of lease between themselves
else
errors.add('It is not possible to purchase many times one item with the same values')
end
end
Check that you have to handle the case where there are different object ids with the same date of lease in the same items array that has duplicates, that should be valid. For example : Item id 1, date 12, Item id 1, date 13, item id 2, date 12 Should be valid.

Sorting using custom value system in ruby

I have an application that dynamically creates a drop-down menu based on certain values in the database. Often the drop-down values are just in the order they come up but I would like to put them in a certain order.
An example of my value system:
Newbie = 0
Amateur = 1
Skilled = 2
Pro = 3
GrandMaster = 4
How would I take the data above and use it to sort an array full of those values (Newbie etc). I've thought about creating a hash of the values but even then I still am not sure how to apply that to the sort method.
Any help would be appreciated.
You can sort this array just by using the usual sorting the sorting won't be done by name it will be done by value. and if these are not integer objects and are some user defined class then sorting based on a particular attribute can be achieved very efficiently by
lst.sort_by &:first
where first is the attribute of the object.
Sort has by value:
hash = {:Newbie=>0, :Amateur=>1, :Skilled=>2, :Pro=>3}
> hash.sort { hash{a} <=> hash{b} }
=> [[:Newbie, 0], [:Amateur, 1], [:Skilled, 2], [:Pro, 3]]
Or use Ruby Hash#sort_by method:
hash.sort_by { |k,v| v }
Suppose you have a Level model that has a sort_id identifying the displayed order and a name holding the displayed name. I recommend using default_scope to set the default order for that model because it is likely that you always want to sort Level records this way:
class Level < ActiveRecord::Base
#### attributes
# id (integer)
# name (string)
# sort_id (integer)
default_scope order('sort_id ASC')
# rest of model...
end
Then, the only thing you have to do in your view to display a picklist is
<%= f.select("level", Level.pluck(:name)) %>
An alternate to #padde. I prefer to avoid default scopes.
class Level < ActiveRecord::Base
#### attributes
# id (integer)
# name (string)
# value (integer)
end
In the view
<%= f.select("level", Level.order(:value).map{|l| [l.name, l.value] } %>
Due to my poorly explained question the others trying to answer my question didn't really get a chance but using their help I did manage to figure out my problem.
ex_array = ["GrandMaster", "Newbie", "Pro", "Skilled", "Amateur"]
value_sys = {:Newbie=>0, :Amateur=>1, :Skilled=>2, :Pro=>3, :GrandMaster=>4}
ex_array.sort { |a,b| value_sys[a.to_sym] <=> value_sys[b.to_sym]
=> ["Newbie", "Amateur", "Skilled","Pro", "GrandMaster"]
Thanks for the help guys. Much appreciated.
Parse your value system for further use:
values = <<EOF
Newbie = 0
Amateur = 1
Skilled = 2
Pro = 3
GrandMaster = 4
EOF
value_map = Hash[values.split("\n").map{|v| v.split(/\s*=\s*/)}.map{|v| [v[0], v[1].to_i]}]
#=> {"Newbie"=>0, "Amateur"=>1, "Skilled"=>2, "Pro"=>3, "GrandMaster"=>4}
Assign the array values a weight according to value_map to transform the array into a new one, sort according to the weight, and then transform the new array back.
# here I created a sample array
array = value_map.keys.shuffle
#=> ["Newbie", "Pro", "Skilled", "Amateur", "GrandMaster"]
# transform and sort
sorted = array.map{|v| [v, value_map[v] || 0xFFFF]}.sort_by{|v| v[1]}.map{|v| v[0]}
#=> ["Newbie", "Amateur", "Skilled", "Pro", "GrandMaster"]
Or you can bypass the transform step and just use the sort_by method:
sorted = array.sort_by{|v| value_map[v] || 0xFFFF}

Display collection value using their attributes name

I want to display value of collection by passing their respective attribute name.
#mandates is the result of an active-record query.
#tabattributes contains array of attribute names previously selected by users.
The code below show field attributes but I want the value of these field instead.
I've tried several syntaxes but errors occurs each time.
How can I modify my code to do that?
#mandates.map do |f|
#tabattributes.each { |att| " #{att} "}
end
If #mandates is a result set that contains models with attributes a, b, and c and #tabattributes is the array %w{a b} (i.e. you want to extract a and b from each element of #mandates) then:
a = #mandates.map { |m| m.attributes.slice(*#tabattributes) }
will give you an array of hashes with keys 'a' and 'b'. For example:
#tabattributes = %w{id created_at}
slices = #mandates.map { |m| m.attributes.slice(*#tabattributes) }
# slices is now like [ { 'id' => ..., 'created_at' => ... }, ... ]
If you only want the values and don't care about the keys then perhaps this will work for you:
#mandates.map { |m| m.attributes.slice(*#tabattributes).values }
That would give you an array-of-arrays. The first array-of-hashes would probably be easier to work with though.
If you can get at #mandates before accessing the database then you could slice out just the columns you're interested inside the database with something like this:
#mandates = Mandate.select(#tabattributes)
slices = #mandates.map(&:attributes)
If I understand you right, you have an array of elements, and you want to have an array containing the name of each element, is that it ? If yes, then array.map {|elem| elem.name} should do it. There is a shorter form (array.map(&:name)) which does the same, if you're interested in how this is working, I can detail.

How to check if specific value is present in a hash?

I'm using Rails and I have a hash object. I want to search the hash for a specific value. I don't know the keys associated with that value.
How do I check if a specific value is present in a hash? Also, how do I find the key associated with that specific value?
Hash includes Enumerable, so you can use the many methods on that module to traverse the hash. It also has this handy method:
hash.has_value?(value_you_seek)
To find the key associated with that value:
hash.key(value_you_seek)
This API documentation for Ruby (1.9.2) should be helpful.
The simplest way to check multiple values are present in a hash is:
h = { a: :b, c: :d }
h.values_at(:a, :c).all? #=> true
h.values_at(:a, :x).all? #=> false
In case you need to check also on blank values in Rails with ActiveSupport:
h.values_at(:a, :c).all?(&:present?)
or
h.values_at(:a, :c).none?(&:blank?)
The same in Ruby without ActiveSupport could be done by passing a block:
h.values_at(:a, :c).all? { |i| i && !i.empty? }
Hash.has_value? and Hash.key.
Imagine you have the following Array of hashes
available_sports = [{name:'baseball', label:'MLB Baseball'},{name:'tackle_football', label:'NFL Football'}]
Doing something like this will do the trick
available_sports.any? {|h| h['name'] == 'basketball'}
=> false
available_sports.any? {|h| h['name'] == 'tackle_football'}
=> true
While Hash#has_key? works but, as Matz wrote here, it has been deprecated in favour of Hash#key?.
Hash's key? method tells you whether a given key is present or not.
hash.key?(:some_key)
The class Hash has the select method which will return a new hash of entries for which the block is true;
h = { "a" => 100, "b" => 200, "c" => 300 }
h.select {|k,v| v == 200} #=> {"b" => 200}
This way you'll search by value, and get your key!
If you do hash.values, you now have an array.
On arrays you can utilize the Enumerable search method include?
hash.values.include?(value_you_seek)
An even shorter version that you could use would be hash.values

Resources