Mongoid access nested attributes with attributes.values_at? - ruby-on-rails

Given the following document (snippet):
{
udid: "0E321DD8-1983-4502-B214-97D6FB046746",
person: {
"firstname": "Jacob",
"lastname": "Prince"
}
}
I'n my console I can basically do:
mycollection.first.attributes.values_at("udid", "person")
This returns the person as a hash.
Now I want a single field. But these doesn't work (person.firstname):
mycollection.first.attributes.values_at("udid", "person.firstname")
mycollection.first.attributes.values_at("udid", "person[:firstname]")
mycollection.first.attributes.values_at("udid", "person['firstname']")
How how do you access the person child-document?
I'm in the need to have users select which fieds they want to export. I was thinking along the lines of doing something like this:
class Foo
include Mongoid::Document
# fields definitions
embeds_one :person # two fields: firstname, lastname
def to_csv *columns
attributes.values_at *columns
end
end

Whats a (the most) efficient way to select specific fields?
If you already know the fields and its nested keys, using Ruby v2.3+ you can utilise the dig() built-in method. For example:
document = collection.find({},{'projection' => {'uid' => 1, "person.firstname" => 1 }}).first
result = [document.dig("uid"), document.dig("person", "firstname")]
puts result.inspect
Alternatively, depending on your application use case you could also utilise MongoDB Aggregation Pipeline, especially $project operator
For example:
document = collection.aggregate([{"$project"=>{ :uid=>"$uid", :person_firstname=>"$person.firstname"}}]).first
puts document.values_at("uid", "person_firstname").inspect
Note that the projection above renames the nested person.firstname into a flatten field called person_firstname.
See also MongoDB Ruby Driver: Aggregation Tutorial

Related

How to access Chewy results with the dot notation?

I'm using Toptal's Chewy gem to connect and query my Elasticsearch, just like an ODM.
I'm using Chewy along with Elasticsearch 6, Ruby on Rails 5.2 and Active Record.
I've defined my index just like this:
class OrdersIndex < Chewy::Index
define_type Order.includes(:customer) do
field :id, type: "keyword"
field :customer do
field :id, type: "keyword"
field :name, type: "text"
field :email, type: "keyword"
end
end
end
And my model:
class Order < ApplicationRecord
belongs_to :customer
end
The problem here is that when I perform any query using Chewy, the customer data gets deserialized as a hash instead of an Object, and I can't use the dot notation to access the nested data.
results = OrdersIndex.query(query_string: { query: "test" })
results.first.id
# => "594d8e8b2cc640bb78bd115ae644637a1cc84dd460be6f69"
results.first.customer.name
# => NoMethodError: undefined method `name' for #<Hash:0x000000000931d928>
results.first.customer["name"]
# => "Frederique Schaefer"
How can I access the nested association using the dot notation (result.customer.name)? Or to deserialize the nested data inside an Object such as a Struct, that allows me to use the dot notation?
try to use
results = OrdersIndex.query(query_string: { query: "test" }).objects
It converts query result into active record Objects. so dot notation should work. If you want to load any extra association with the above result you can use .load method on Index.
If you want to convert existing ES nested object to accessible with dot notation try to reference this answer. Open Struct is best way to get things done in ruby.
Unable to use dot syntax for ruby hash
also, this one can help too
see this link if you need openStruct to work for nested object
Converting the just-deserialized results to JSON string and deserializing it again with OpenStruct as an object_class can be a bad idea and has a great CPU cost.
I've solved it differently, using recursion and the Ruby's native Struct, preserving the laziness of the Chewy gem.
def convert_to_object(keys, values)
schema = Struct.new(*keys.map(&:to_sym))
object = schema.new(*values)
object.each_pair do |key, value|
if value.is_a?(Hash)
object.send("#{key}=", convert_to_object(value.keys, value.values))
end
end
object
end
OrdersIndex.query(query_string: { query: "test" }).lazy.map do |item|
convert_to_object(item.attributes.keys, item.attributes.values)
end
convert_to_object takes an array of keys and another one of values and creates a struct from it. Whenever the class of one of the array of values items is a Hash, then it converts to a struct, recursively, passing the hash keys and values.
To presence the laziness, that is the coolest part of Chewy, I've used Enumerator::Lazy and Enumerator#map. Mapping every value returned by the ES query into the convert_to_object function, makes every entry a complete struct.
The code is very generic and works to every index I've got.

How to get rid of surrounding quotes in Rails?

I'm having problems with weird behaviour in RoR. I'm having a Hash that i'm converting to json using to_json() like so:
data = Hash.new
# ...
data = data.to_json()
This code appears inside a model class. Basically, I'm converting the hash to JSON when saving to database. The problem is, the string gets saved to database with its surrounding quotes. For example, saving an empty hash results in: "{}". This quoted string fails to parse when loading from the database.
How do I get rid of the quotes?
The code is:
def do_before_save
#_data = self.data
self.data = self.data.to_json()
end
EDIT:
Due to confusions, I'm showing my entire model class
require 'json'
class User::User < ActiveRecord::Base
after_find { |user|
user.data = JSON.parse(user.data)
}
after_initialize { |user|
self.data = Hash.new unless self.data
}
before_save :do_before_save
after_save :do_after_save
private
def do_before_save
#_data = self.data
self.data = self.data.to_json()
end
def do_after_save
self.data = #_data
end
end
The data field is TEXT in mysql.
I'm willing to bet money that this is the result of you calling .to_json on the same data twice (without parsing it in between). I've had a fair share of these problems before I devised a rule: "don't mutate data in a lossy way like this".
If your original data was {}, then first .to_json would produce "{}". But if you were to jsonify it again, you'd get "\"{}\"" because a string is a valid json data type.
I suggest that you put a breakpoint in your before_save filter and see who's calling it the second time and why.
Update
"call .to_json twice" is a general description and can also mean that there are two subsequent saves on the same object, and since self.data is reassigned, this leads to data corruption. (thanks, #mudasobwa)
It depends on your model's database field type.
If the field is string type (like VARCHAR or TEXT) it should be stored as string (no need to get rid of the quotes - they are fine). Make sure calling to_json once.
If the field is Postgres JSON type, then you can just assign a hash to the model's field, no need to call to_json at all.
If you are saving hash as a JSON string in a varchar column you can use serialize to handle marshalling/unmarshaling the data:
class Thing < ActiveRecord::Base
serialize :foo, JSON
end
Knowing exactly when to convert the data in the lifecycle of a record is actually quite a bit harder than your naive implementation. So don't reinvent the wheel.
However a huge drawback is that the data cannot be queried in the DB*. If you are using Postgres or MySQL you can instead use a JSON or JSONB (postgres only) column type which allows querying. This example is from the Rails guide docs:
# db/migrate/20131220144913_create_events.rb
create_table :events do |t|
t.json 'payload'
end
# app/models/event.rb
class Event < ApplicationRecord
end
# Usage
Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
event = Event.first
event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
## Query based on JSON document
# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text
Event.where("payload->>'kind' = ?", "user_renamed")
use {}.as_json instead of {}.to_json
ex:
a = {}
a.as_json # => {}
a.to_json # => "{}"
http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json

Rails elasticsearch - named scope search

I'm just moving over from the Tire gem to the official elasticsearch Ruby wrapper and am working on implementing better search functionality.
I have a model InventoryItem and a model Store. Store has_many :inventory_items. I have a model scope on Store called local
scope :local, lambda{|user| near([user.latitude, user.longitude], user.distance_preference, :order => :distance)}
I want the search to only return results from this scope so I tried: InventoryItem.local(user).search.... but it searches the entire index, not the scope. After doing some research, it looks like filter's are a good way to achieve this, but I'm unsure how to implement. I'm open to other ways of achieving this as well. My ultimate goal is be able to search a subset of the InventoryItem model based on store location.
Another thing you can do is to send the list of valid ids right to elastic, so it will filter records out by itself and then perform a search on ones that left. We were not doing tests whether it is faster yet, but I think it should, because elastic is a search engine after all.
I'll try to compose an example using you classes + variables and our experience with that:
def search
# retrieve ids you want to perform search within
#store_ids = Store.local(current_user).select(:id).pluck(:id)
# you could also check whether there are any ids available
# if there is none - no need to request elastic to search
#response = InventoryItem.search_by_store_ids('whatever', #store_ids)
end
And a model:
class InventoryItem
# ...
# search elastic only for passed store ids
def self.search_by_store_ids(query, store_ids, options = {})
# use method below
# also you can use it separately when you don't need any id filtering
self.search_all(query, options.deep_merge({
query: {
filtered: {
filter: {
terms: {
store_id: store_ids
}
}
}
}
}))
end
# search elastic for all inventory items
def self.search_all(query, options = {})
self.__elasticsearch__.search(
{
query: {
filtered: {
query: {
# use your fields you want to search, our's was 'text'
match: { text: query },
},
filter: {},
strategy: 'leap_frog_filter_first' # do the filter first
}
}
}.deep_merge(options)
# merge options from self.search_by_store_ids if calling from there
# so the ids filter will be applied
)
end
# ...
end
That way you also have to index store_id.
You can read more about filters here.
I'll leave this answer without accepting until the bounty is over - feel free to add an answer if you think you've found a better solution that one below.
The answer to this ended up being fairly simple after some digging.
Using the named scope:
scope :for_stores, lambda{ |stores| where(store_id: stores.map(&:id)) }
My controller method:
def search
#stores = Store.local(current_user) # get local stores
response = InventoryItem.search 'whatever' # execute the search
#inventory_items = response.records.for_stores(#stores) # call records
end
On elasticsearch responses, you can either call records or results. Calling just results will simply yield the results from the index that you can display etc. Calling records actually pulls the AR records which allows you to chain methods like I did above. Cool! More info in the docs obviously.

Ruby on Rails - Validations and Before Filter method to substitute User Inputted Values

In my Ruby on Rails app, I have a sign-up form, where users have to enter some data. I have strict validations that only allow entered values that are members of an array. This isn't part of my app, but it uses the same concept I want to apply.
Say I wanted to have a field where a user entered a superhero name. My validations would have an array like so.
SUPERHEROES = ['Batman', 'Superman', 'Captain America', 'Wonder Woman', 'Spiderman']
validates_inclusion_of :superhero, :in => SUPERHEROES
If a user entered Clark Kent, for example, the validations would fail. Given I created a new array.
ALIASES = ['Bruce Wayne', 'Clark Kent', 'Steven Rogers', 'Princess Diana', 'Peter Parker']
I'd like before the form is submitted (update action) for the values in the ALIASES array to be converted into the SUPERHEROES array.
I was thinking something like this could work.
def alias_to_superhero
ALIASES.each_do |alias|
i = 0
while i < SUPERHEROES.length
alias.gsub(alias, "#{SUPERHEROES[i]}")
i++
end
end
end
And then at the top of my validations final I could have a line like this
before_update: alias_to_superhero
Any suggestions?
You can validate that superhero contains the hero or the alias name of an superhero. But after validation before save you replace aliases with the matching hero name. The benefit of replacing the alias with the hero name after validation is that you keep the users input untouched unless all validations succeed.
validates_inclusion_of :superhero, :in => SUPERHEROES + ALIAS
before_save :replace_alias_with_hero_name
private
def replace_alias_with_hero_name
if ALIAS.include?(superhero)
self.superhero = SUPERHEROES[ALIAS.find_index(superhero)]
end
end
This solution only work when both arrays have the same size and the hero names and aliases are at the same position in the array. A more flexable version would perhaps operate on a hash like this:
HEROS => { 'SUPERMAN' => ['Clark Kent', 'C. Kent' ... ] ... }
You will have to use before_validation callback instead of before_update
before_validation: alias_to_superhero
Also you can check directly against inclusion of ALIASES or have a join array of both ALIASES and SUPERHEROS for validity.

Struct with types and conversion

I am trying to accomplish the following in Ruby:
person_struct = StructWithType.new "Person",
:name => String,
:age => Fixnum,
:money_into_bank_account => Float
And I would like it to accept both:
person_struct.new "Some Name",10,100000.0
and
person_struct.new "Some Name","10","100000.0"
That is, I'd like it to do data conversion stuff automatically.
I know Ruby is dinamically and I should not care about data types but this kind of conversion would be handy.
What I am asking is something similar to ActiveRecord already does: convert String to thedatatype defined in the table column.
After searching into ActiveModel I could not figure out how to to some TableLess that do this conversion.
After all I think my problem may require much less that would be offered by ActiveModel modules.
Of course I could implement a class by myself that presents this conversion feature, but I would rather know this has not yet been done in order to not reinvent the wheel.
Tks in advance.
I think that the implementation inside a class is so easy, and there is no overhead at all, so I don't see the reason to use StructWithType at all. Ruby is not only dynamic, but very efficient in storing its instances. As long as you don't use an attribute, there is none.
The implementation in a class should be:
def initialize(name, age, money_into_bank_account)
self.name = name
self.age = age.to_i
self.money_into_bank_account = money_into_bank_account.to_f
end
The implementation in StructWithType would then be one layer higher:
Implement for each type a converter.
Bind an instance of that converter in the class.
Use in the new implementation of StructWithType instances (not class) the converters of the class to do the conversion.
A very first sketch of it could go like that:
class StructWithType
def create(args*)
<Some code to create new_inst>
args.each_with_index do |arg,index|
new_value = self.converter[index].convert(arg)
new_inst[argname[index]]= new_value
end
end
end
The ideas here are:
You have an instance method named create that creates from the factory a new struct instance.
The factory iterates through all args (with the index) and searches for each arg the converter to use.
It converts the arg with the converter.
It stores in the new instance at the argname (method argname[] has to be written) the new value.
So you have to implement the creation of the struct, the lookup for converter, the lookup for the argument name and the setter for the attributes of the new instance. Sorry, no more time today ...
I have used create because new has a different meaning in Ruby, I did not want to mess this up.
I have found a project in github that fulfill some of my requirements: ActiveHash.
Even though I still have to create a class for each type but the type conversion is free.
I am giving it a try.
Usage example:
class Country < ActiveHash::Base
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
country = Country.new(:name => "Mexico")
country.name # => "Mexico"
country.name? # => true

Resources