I have the following model in rails:
class Model < ActiveRecord::Base
# id — integer
# name — string
# model_id — integer
belongs_to :parent, class_name: 'Model', foreign_key: 'model_id'
has_many :children, class_name: 'Model', foreign_key: 'model_id'
end
I am using adjacency structure, which can have infinite depth. I am on a Postgres database using recursive selects.
What will be the most sane way to get a nested hash of objects? I tried to select instances of Model and sort them, yet could not bring this to any usable result.
Lets say I have four Model instances saved in my database: Model_1, Model_2, Model_3 and Model_4. Model_3 is a child of Model_2 and Model_4 is a child of Model_3.
Here is an output I am trying to achieve (a nested hash of Model instances):
{
#<Model_1...> => {},
#<Model_2...> => {
#<Model_3...> => {
#<Model_4...> => {}
}
}
}
Any ideas?
Update: Tree is already recovered — either as a CollectionProxy, Relation or any other array-ish data structure. I wan't to sort that tree into the hash of nested hashes.
I would name it parent_id field.
belongs_to :parent, class_name: "Model"
has_many :children, class_name: "Model", foreign_key: "parent_id"
When you have the hash, you would use sort or sort_by:
http://www.ruby-doc.org/core-2.1.0/Enumerable.html#method-i-sort_by
def sort(hash)
hash.sort { |m1, m2| m1.id <=> m2.id }
sort(hash.children)
end
First, define the ff method inside your Model class:
def to_hash
if children.empty?
{self => {}}
else
{self => children.inject({}) {|hash, model| hash.merge(model.to_hash) } }
end
end
Then do the ff to get the output you want:
top_level_models = #code that queries top-level models while eager-loading all nested children
hash_of_nested_models = top_level_models.inject({}) {|hash, ancestor| hash.merge(ancestor.to_hash) }
The hash argument that you pass to includes should cover the depth of your nesting. The argument passed to includes above will be the nesting for children with a depth of 3 descendants. For as long as you includes all the nested children in your where query, generating the hash will not do any more db queries.
Hope that helps!
Getting AR to do this without N+1 queries would be difficult. Will have to write something that works with the data in memory.
You would have to write a custom function which looks something like:
def to_hash
root_hash = {}
# Load all the model into memory and into a hash to allow faster access when we have the id
models = Hash[Model.all.collect {|m| [m.id, m]}]
# The resultant hash for each child
models_with_hash = Hash[map.values.collect {|m| [m.id, {}]} ]
# Stitch them together
models.each do |id, m|
if m.model_id.nil?
root_hash.merge! m => models_with_hash[m.id]
else
# should name model_id to parent_id
models_with_hash[m.model_id].merge! m => models_with_hash[m.id]
end
end
root_hash
end
Related
I am trying to make a scope for my EventsLog model which looks something along the lines of EventsLog.with_values({"value_name" => "value", "other_value_name" => "other_value"}).
The results of which would be the EventsLog records that have an associated EventsLogValue for each of the key-value pairs in the hash.
Here is what I have to work with.
Two tables whose definitions look like this:
--table for tracking events
CREATE TABLE events_log(
id INT PRIMARY KEY IDENTITY(1,1),
event_name VARCHAR(25), --name of the event
created_at DATETIME
);
--table for tracking the values corresponding to the event
CREATE TABLE events_log_values(
id INT PRIMARY KEY IDENTITY(1,1),
event_id INT,
value VARCHAR(255),
value_name VARCHAR(25),
);
From these two tables two models which look like:
class EventsLog < BaseAPIDatabase
self.table_name = "events_log"
self.primary_key = "id"
has_many :events_log_values, :foreign_key => "event_id", :primary_key => "id", :class_name => "EventsLogValue", :autosave => true
scope :since, ->(since){ where("created_at > ?", since)}
scope :named, ->(event_name){ where(:event_name => event_name) }
def values
events_log_values.inject({}) do |hsh, v|
hsh.merge({v.value_name => v.value})
end
end
end
class EventsLogValue < BaseAPIDatabase
self.table_name = "events_log_values"
self.primary_key = "id"
end
My approach so far has been to try and create a function that returns an active record relation which has applied one key-value pair at a time and then later to add a scope (or probably just a class method returning a relation) which chains them for me (something along the lines of scope :with_values, ->(values){values.inject(self){|slf, (k, v)| slf.with_value(k, v)} }).
Originally I tried to implement with_value as a fairly standard scope, scope :with_value, ->(val_name, val){ eager_load(:events_log_values).where(:events_log_values => {:value_name => val_name, :value => val}) }, which works fine by itself but when chained results in a single join with multiple conditions on the joined values.
Deciding that this would be solved by joining the values table with an alias for each condition; my new approach has been to define a has_many association in my with_value function then to eager_load that association and to add a where condition based on each new association:
def self.with_value(val_name, val)
has_many val_name.to_sym, ->(){ where(:value_name => val_name) }, :foreign_key => "event_id", :primary_key => "id", :class_name => "EventsLogValue"
res = eager_load(:events_log_values)
res.eager_load(val_name.to_sym).where("#{val_name.pluralize}_events_log" => {:value => val})
end
This actually works pretty well but has a few problems. The first one being that I have a difficult time knowing what the name in the where condition is going to be for the association. The second (and bigger problem) being that my values function now only has whatever value_names have not had an association made for them.
Here is some sql which was generated by the multiple has_manys and may help to illustrate what I am trying to do:
EventsLog.with_values("hello" => "world", "foo" => "bar").to_sql
SELECT ...
FROM [events_log]
LEFT OUTER JOIN [events_log_values] ON [events_log_values].[event_id] = [events_log].[id]
LEFT OUTER JOIN [events_log_values] [hellos_events_log] ON [hellos_events_log].[event_id] = [events_log].[id] AND [hellos_events_log].[value_name] = 'hello'
LEFT OUTER JOIN [events_log_values] [foos_events_log] ON [foos_events_log].[event_id] = [events_log].[id] AND [foos_events_log].[value_name] = 'foo'
WHERE [hellos_events_log].[value] = 'world' AND [foos_events_log].[value] = 'bar'
How can I go about getting a record which has several associated records meeting several separate conditions?
This is the answer I have been able to come up with since asking my question. It uses arel to generate sql joins with aliases for each of the values and also generates a where condition for each value.
It's not the cleanest thing but it seems to get the job done.
def self.with_values(values)
el = EventsLog.arel_table
arel_joins = el
arel_wheres = []
values.each do |k, v|
ev = EventsLogValue.arel_table.alias("#{k}_join")
arel_joins = arel_joins.join(ev).on(el[:id].eq(ev[:event_id]).and(ev[:value_name].eq(k)))
arel_wheres << ev[:value].eq(v)
end
arel_wheres.inject(EventsLog.joins(arel_joins.join_sources)){|rel, con| rel.where(con)}
end
p.s. I think I read somewhere that Model.arel_table is undocumented and should not be used? It may be prudent to use Arel::Table.new('table_name') instead.
Rails 4.2.5, Mongoid 5.1.0
I have three models - Mailbox, Communication, and Message.
mailbox.rb
class Mailbox
include Mongoid::Document
belongs_to :user
has_many :communications
end
communication.rb
class Communication
include Mongoid::Document
include Mongoid::Timestamps
include AASM
belongs_to :mailbox
has_and_belongs_to_many :messages, autosave: true
field :read_at, type: DateTime
field :box, type: String
field :touched_at, type: DateTime
field :import_thread_id, type: Integer
scope :inbox, -> { where(:box => 'inbox') }
end
message.rb
class Message
include Mongoid::Document
include Mongoid::Timestamps
attr_accessor :communication_id
has_and_belongs_to_many :communications, autosave: true
belongs_to :from_user, class_name: 'User'
belongs_to :to_user, class_name: 'User'
field :subject, type: String
field :body, type: String
field :sent_at, type: DateTime
end
I'm using the authentication gem devise, which gives access to the current_user helper, which points at the current user logged in.
I have built a query for a controller that satisfied the following conditions:
Get the current_user's mailbox, whose communication's are filtered by the box field, where box == 'inbox'.
It was constructed like this (and is working):
current_user.mailbox.communications.where(:box => 'inbox')
My issue arrises when I try to build upon this query. I wish to chain queries so that I only obtain messages whose last message is not from the current_user. I am aware of the .last method, which returns the most recent record. I have come up with the following query but cannot understand what would need to be adjusted in order to make it work:
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
This query produces the following result:
undefined method 'from_user' for #<Origin::Key:0x007fd2295ff6d8>
I am currently able to accomplish this by doing the following, which I know is very inefficient and want to change immediately:
mb = current_user.mailbox.communications.inbox
comms = mb.reject {|c| c.messages.last.from_user == current_user}
I wish to move this logic from ruby to the actual database query. Thank you in advance to anyone who assists me with this, and please let me know if anymore information is helpful here.
Ok, so what's happening here is kind of messy, and has to do with how smart Mongoid is actually able to be when doing associations.
Specifically how queries are constructed when 'crossing' between two associations.
In the case of your first query:
current_user.mailbox.communications.where(:box => 'inbox')
That's cool with mongoid, because that actually just desugars into really 2 db calls:
Get the current mailbox for the user
Mongoid builds a criteria directly against the communication collection, with a where statement saying: use the mailbox id from item 1, and filter to box = inbox.
Now when we get to your next query,
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
Is when Mongoid starts to be confused.
Here's the main issue: When you use 'where' you are querying the collection you are on. You won't cross associations.
What the where(:messages.last.from_user => {'$ne' => current_user}) is actually doing is not checking the messages association. What Mongoid is actually doing is searching the communication document for a property that would have a JSON path similar to: communication['messages']['last']['from_user'].
Now that you know why, you can get at what you want, but it's going to require a little more sweat than the equivalent ActiveRecord work.
Here's more of the way you can get at what you want:
user_id = current_user.id
communication_ids = current_user.mailbox.communications.where(:box => 'inbox').pluck(:_id)
# We're going to need to work around the fact there is no 'group by' in
# Mongoid, so there's really no way to get the 'last' entry in a set
messages_for_communications = Messages.where(:communications_ids => {"$in" => communications_ids}).pluck(
[:_id, :communications_ids, :from_user_id, :sent_at]
)
# Now that we've got a hash, we need to expand it per-communication,
# And we will throw out communications that don't involve the user
messages_with_communication_ids = messages_for_communications.flat_map do |mesg|
message_set = []
mesg["communications_ids"].each do |c_id|
if communication_ids.include?(c_id)
message_set << ({:id => mesg["_id"],
:communication_id => c_id,
:from_user => mesg["from_user_id"],
:sent_at => mesg["sent_at"]})
end
message_set
end
# Group by communication_id
grouped_messages = messages_with_communication_ids.group_by { |msg| mesg[:communication_id] }
communications_and_message_ids = {}
grouped_messages.each_pair do |k,v|
sorted_messages = v.sort_by { |msg| msg[:sent_at] }
if sorted_messages.last[:from_user] != user_id
communications_and_message_ids[k] = sorted_messages.last[:id]
end
end
# This is now a hash of {:communication_id => :last_message_id}
communications_and_message_ids
I'm not sure my code is 100% (you probably need to check the field names in the documents to make sure I'm searching through the right ones), but I think you get the general pattern.
Given I have two models:
class Post < ActiveRecord::Base
belongs_to :district
end
class District < ActiveRecord::Base
has_many :posts
end
I need to make a check_boxes filter in ActiveAdmin on Posts page for with ability for user to get posts that belong to some exact districts or does not belong to any districts at all.
Before ActiveAdmin changed MetaSearch to Ransack, that could be done with custom scope. And now I don't have any idea. When I do the following:
filter :district_id_null, as: :boolean
filter :district, as: :check_boxes
It makes condition WHERE district_id IN (1,2,3) AND district_id IS NULL (I need OR instead of AND). And when I do
filter :district, as: :check_boxes, collection: proc { District.unscoped.map { |x| [x.title, x.id] }.unshift ['Empty', 'null'] }
It makes condition WHERE district_id IN (0,1,2,3) (but in most SQL databases NULL is not 0).
I think something like this might work
class Post
def self.ransack_with_or(search_params)
params = search_params.deep_clone
#check if we need OR
if search_params.has_key?('district_id_in') && search_params.has_key?('district_id_null')
params.delete('district_id_null')
district_id_in = params.delete('district_id_in')
#old behaviour without district_id_null and district_id_null attributes
q = ransack_without_or(params)
#here we're adding OR group
q.build_grouping({m: 'or', district_id_null: true, district_id_in: district_id_in})
else
#old behaviour we don't need OR
q = ransack_without_or(params)
end
q
end
#wrapping ransack method
class << self
alias_method_chain :ransack, :or
end
end
I have two models:
class Wine
belongs_to :region
end
class Region
has_many :wines
end
I am attempting to use the #where method with a hash built from transforming certain elements from the params hash into a query hash, for example { :region => '2452' }
def index
...
#wines = Wine.where(hash)
...
end
But all I get is a column doesn't exist error when the query is executed:
ActiveRecord::StatementInvalid: PGError: ERROR: column wines.region does not exist
LINE 1: SELECT "wines".* FROM "wines" WHERE "wines"."region" =...
Of course, the table wines has region_id so if I queried for region_id instead I would not get an error.
The question is the following:
Is there a rails-y way to query the Wine object for specific regions using the id in the #where method? I've listed some options below based on what I know I can do.
Option 1:
I could change the way that I build the query hash so that each field has _id (like { :region_id => '1234', :varietal_id => '1515' } but not all of the associations from Wine are belongs_to and thus don't have an entry in wines for _id, making the logic more complicated with joins and what not.
Option 2:
Build a SQL where clause, again using some logic to determine whether to use the id or join against another table... again the logic would be somewhat more complicated, and delving in to SQL makes it feel less rails-y. Or I could be wrong on that front.
Option(s) 3..n:
Things I haven't thought about... your input goes here :)
You could set up a scope in the Wine model to make it more rails-y ...
class Wine < ActiveRecord::Base
belongs_to :region
attr_accessible :name, :region_id
scope :from_region, lambda { |region|
joins(:region).where(:region_id => region.id)
}
end
So then you can do something like:
region = Region.find_by_name('France')
wine = Wine.from_region(region)
Edit 1:
or if you want to be really fancy you could do a scope for multiple regions:
scope :from_regions, lambda { |regions|
joins(:region).where("region_id in (?)", regions.select(:id))
}
regions = Region.where("name in (?)", ['France','Spain']) # or however you want to select them
wines = Wine.from_regions(regions)
Edit 2:
You can also chain scopes and where clauses, if required:
regions = Region.where("name in (?)", ['France','Spain'])
wines = Wine.from_regions(regions).where(:varietal_id => '1515')
Thanks to all who replied. The answers I got would be great for single condition queries but I needed something that could deal with a varying number of conditions.
I ended up implementing my option #1, which was to build a condition hash by iterating through and concatenating _id to the values:
def query_conditions_hash(conditions)
conditions.inject({}) do |hash, (k,v)|
k = (k.to_s + "_id").to_sym
hash[k] = v.to_i
hash
end
end
So that the method would take a hash that was built from params like this:
{ region => '1235', varietal => '1551', product_attribute => '9' }
and drop an _id onto the end of each key and change the value to an integer:
{ region_id => 1235, varietal_id => 1551, product_attribute_id => 9 }
We'll see how sustainable this is, but this is what I went with for now.
I have model Item and model Stats.
Item
has_many :stats
Stat
belongs_to :items
In the model (e.g. mysql table) Stat there is 3 fields:
rating
skin_id
item_id
So for Stat, it could be, like:
#item.stats => Array of stats for records with item_id = 1, with a differer skin_id
I need to sort all items, for a given skin_id by 'rating'.
Something like:
#items = Item.all.order('stats[currtnt_skin.id] DESC') (of course it doesn't work)
In other words i need to sort within array of:
#stats = #items.stats[current_skin.id]
#items.order (... by #stats ...)
How it could be done?
Firstly I'm presuming by belongs_to :items you mean belongs_to :item (singular) given the presence of the item_id foreign key.
Secondly, to solve your specific query you can use:
Stat.where(:skin_id => skin_id).joins(:item).order("items.rating DESC")
However, if skin_id refers to another model - i.e. Stat belongs_to :skin and Skin has_many :stats then it may make more sense to start from there:
skin = Skin.find(1)
stats = skin.stats.order("rating DESC").includes(:item)
To get the items then just loop through them:
stats = skin.stats.order("rating DESC").includes(:item)
stats.each do |stat|
stat.item
end
F
#items = Item.join(:stats).order('skin_id DESC')
I believe, though I might be mistaken that joining the table will do so on the association you've defined.
in rails 3 it will be something like:
Item.includes("stats").order("stats.skin_id desc")
Have you tried this ?
Item.includes("stats").where('stats.skin_id = ?', 1).order("stats.rating desc")