Is it possible to perform a query and return the embedded documents?
Currently, I have:
class Post
include MongoMapper::Document
many :comments
end
class Comment
include MongoMapper::EmbeddedDocument
belongs_to :post
key :author
key :date
key :body
end
Here is a query that is almost there:
Post.all("comments.date" => {"$gt" => 3.days.ago})
This will return all the post objects but not the comments. I guess I could do something like:
Post.all("comments.date" => {"$gt" => 3.days.ago}).map(&:comments)
But this would return all comments from the posts. I'd like to get all of the comments that met this condition. Perhaps Comment should not be embedded.
I assume you're looking for all comments newer than three days ago? Since your Comments are just embedded documents, they do not exist without the Post object so there's no way to "query" them separately (this is actually a future feature of MongoDB). However, you could easily add a convenience method to help you out:
class Comment
include MongoMapper::EmbeddedDocument
def self.latest
Post.all(:conditions => {"comments.date" => {"$gt" => 3.days.ago}}).map{|p| p.comments}.flatten
end
end
This method would get you all of the comments that have been updated in the last three days but they would not entirely be in order. A better solution might be to use Map/Reduce to pull the latest comments:
class Comment
include MongoMapper::EmbeddedDocument
def self.latest
map = <<-JS
function(){
this.comments.forEach(function(comment) {
emit(comment.created_at, comment)
});
}
JS
r = "function(k,vals){return 1;}"
q = {'comments.created_at' => {'$gt' => 3.days.ago}}
Post.collection.map_reduce(m,r,:query => q).sort([['$natural',-1]])
end
end
Caveat: the above is completely untested code and just exists as an example, but theoretically should return all comments from the last three days sorted in descending order.
Related
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.
Here are my classes, I am following the example provided at: https://github.com/neo4jrb/neo4j/tree/master/example/blog
The application so far works for CRUD operations.
Here are my model classes:
class Post
include Neo4j::ActiveNode
property :title
property :description
validates :title, presence: true
index :title
has_many :out, :comments, rel_class: PostComment
end
class Comment
include Neo4j::ActiveNode
property :body
index :body
has_one :in, :post, rel_class: PostComment
end
class PostComment
include Neo4j::ActiveRel
from_class Post
to_class Comment
type 'has_comments'
property :created_at
end
I have 2 Posts. Post 1 has 3 Comments and Post 2 has one Comment. For comments in total.
When I a run this query from the neo4j-shell I am getting the right number of records.
neo4j-sh (?)$ start n=node(*) match n-[:has_comments]->(m) return n,m;
4 rows
30 ms
This is correct.
Now trying something similar form RoR I am getting 8 rows instead of 4.
2.1.5 :012 > result = Post.query_as(:post).match("posts-[:has_comments]->(comment:Comment)").pluck(:post, :comment)
2.1.5 :014 > result.count
=> 8
Can't see what is wrong here. Any help will be much appreciated.
I'd expect 6 rows, not 8, but maybe I'm overlooking something. Doesn't matter, really. Instead of returning post and comment independently, try returning post and collections of comment. You don't need query_as, either, you can do it purely through QueryProxy.
result = Post.as(:post).comments(:comment).pluck(:post, 'collect(comment)')
result.each do |pair|
post = pair[0]
comments_array = pair[1]
comments_array.each do |comment|
# do things
end
end
We're going to have an includes method in soon that will automate most of this for you, you'll just be able to do Post.all.includes(:comments) and then post.comments.each won't result in an extra query.
Also note that the match you wrote in wrote in the shell isn't the same as what the gem is going to generate. Call to_cypher on your query prior to pluck to see what it's building. It own't include the return statement, so add that on: RETURN post, comment if you want to do it as is. The gem's Cypher is a little messy as of the latest release but it'll be cleaner in the next one.
Really been struggling trying to get a group by to work when I have to join to another table. I can get the group by to work when I don't join, but when I want to group by a column on the other table I start having problems.
Tables:
Book
id, category_id
Category
id, name
ActiveRecord schema:
class Category < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :category
end
I am trying to get a group by on a count of categories. I.E. I want to know how many books are in each category.
I have tried numerous things, here is the latest,
books = Book.joins(:category).where(:select => 'count(books.id), Category.name', :group => 'Category.name')
I am looking to get something back like
[{:name => fiction, :count => 12}, {:name => non-fiction, :count => 4}]
Any ideas?
Thanks in advance!
How about this:
Category.joins(:books).group("categories.id").count
It should return an array of key/value pairs, where the key represents the category id, and the value represents the count of books associated with that category.
If you're just after the count of books in each category, the association methods you get from the has_many association may be enough (check out the Association Basics guide). You can get the number of books that belong to a particular category using
#category.books.size
If you wanted to build the array you described, you could build it yourself with something like:
array = Categories.all.map { |cat| { name: cat.name, count: cat.books.size } }
As an extra point, if you're likely to be looking up the number of books in a category frequently, you may also want to consider using a counter cache so getting the count of books in a category doesn't require an additional trip to the database. To do that, you'd need to make the following change in your books model:
# books.rb
belongs_to :category, counter_cache: true
And create a migration to add and initialize the column to be used by the counter cache:
class AddBooksCountToCategories < ActiveRecord::Migration
def change
add_column :categories, :books_count, :integer, default: 0, null: false
Category.all.each do |cat|
Category.reset_counters(cat.id, :books)
end
end
end
EDIT: After some experimentation, the following should give you close to what you want:
counts = Category.joins(:books).count(group: 'categories.name')
That will return a hash with the category name as keys and the counts as values. You could use .map { |k, v| { name: k, count: v } } to then get it to exactly the format you specified in your question.
I would keep an eye on something like that though -- once you have a large enough number of books, the join could slow things down somewhat. Using counter_cache will always be the most performant, and for a large enough number of books eager loading with two separate queries may also give you better performance (which was the reason eager loading using includes changed from using a joins to multiple queries in Rails 2.1).
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 such code
#pre_articles = Article.find(:all, :conditions => { :ART_ID => #linkla.map(&:LA_ART_ID)})
#articles = Kaminari.paginate_array(#pre_articles).page(params[:page]).per(15)
It's selecting for me array of data, but it's huge, so i decided to add pagination. It select's 15 entries for view, but also for every page (in log) i see, how sql is selecting all array as in first #pre_articles. For more speed: how to select on every page for example 0-15, 15-30, 30-45 etc entries and send it for view? now it's selecting all data, but dislpaying as i need
Oh sorry, important!:
#linkla = LinkArt.where(:LA_ID => #la_typs.map(&:LAT_LA_ID), :LA_GA_ID => #genart.map(&:GA_ID))
#articles = Article.where(:ART_ID => #linkla.map(&:LA_ART_ID)).page(params[:page]).per(15)
So looks my query. As you see depending on #linkla results i select articles, and link la is selecting many as before... how to do that he select only for my page
Solution to the stated problem:
LinkType
has_many :links
# let us assume there is a column called name
# I wasn't sure about this model from your description
GenArt
has_many :links
# let us assume there is a column called name
Link
belongs_to :article
belongs_to :link_type
belongs_to :gen_art
# columns: id, article_id, link_type_id, gen_art_id
Article
has_many :links
Assuming you the params hash contains link_type_names and gen_art_names
Article.joins(:links => [:link_type, :gen_art]).where(
:links => {
:link_type => {:name => params[:link_type_names]},
:link_type => {:name => params[:gen_art_names]}
}
).page(7).per(50)
What about using where clause instead of conditional find?
#articles = Article.where(:ART_ID: #linkla.map(&:LA_ART_ID)).page(params[:page]).per(15)
The SQL query generated will include a LIMIT clause to avoid loading unnecessary data.