Neo4j Cypher: How to fetch nodes with conditional query on reation - ruby-on-rails

I have two Neo4j Nodes and one relation:
class StayPal
include Neo4j::ActiveNode
has_many :in, :places, origin: :owner
has_many :in, :shared_places, rel_class: 'HouseMate'
end
class Place
include Neo4j::ActiveNode
has_one :out, :owner, type: :owner_of, model_class: 'StayPal'
has_many :out, :house_mates, rel_class: 'HouseMate'
end
class HouseMate
include Neo4j::ActiveRel
include Enumable
creates_unique
from_class 'Place'
to_class 'StayPal'
type 'shared_with'
property :status, default: 0
enum_attr status: [:pending, :approved, :declined, :cancelled]
end
Objective: My objective is get places & shared_places of staypal together but the shared places included if they are status == approved
Query:
Neo4j::Session.current.query
.match(n: { StayPal: { user_id: 1 } })
.match('n<-[rel1:`owner_of`]-(result_places:`Place`)')
.match('n<-[rel2:`shared_with`]-(result_places1:`Place`)')
.pluck(:result_places1, :result_places)
With this I am getting the places and shared places of staypal
But I want shared places where status = 1
Modified Query
Neo4j::Session.current.query
.match(n: { StayPal: { user_id: 1 } })
.match('n<-[rel1:`owner_of`]-(result_places:`Place`)')
.match('n<-[rel2:`shared_with`]-result_places1:`Place`)')
.where('result_places1.status = 1')
.pluck(:result_places1, :result_places)
But with this I am getting no records
Some other helping queries
Neo4j::Session.current.query
.match(n: { StayPal: { user_id: 1 } })
.match('n<-[rel1:`owner_of`]-(result_places:`Place`)')
.match('n<-[rel2:`shared_with`]-result_places1:`Place`)')
.where('result_places1.status = 1')
.pluck(:result_places1)
Output:
[CypherRelationship 1239]

You are using the neo4j-core Query API, which you can do and which should allow you to get ActiveNode and ActiveRel objects, but there is a higher level API which is easier:
StayPal.where(user_id: 1).places(:place1).shared_places(:places2).pluck(:place1, place2)
To do this I assumed that you add this association to Place:
has_many :both, :shared_places, type: :shared_with
Note that I used the singular form for variables. It's important to remember that when you are matching that it does one match at a time. Singular variables help us to keep that in context.
Aside from that, though, I think you have a deeper issue that your HouseMate relationship is going from a Place to a StayPal. What is your model? If you want to record two people staying in the same house, you might want to have a new node with a label like HouseMateGroup and that node could point to the Place as well as two (or more) StayPals.
EDIT:
I think I'm understanding your use case more. I would probably make the model (:StayPal)-[:LIVES_IN]->(:Place)<-[:LIVES_IN]-(:StayPal)
Any given step in that doesn't map to the idea of a "house mate", but you can easily get the housemates by following relationships/associations. So if you wanted to get housemates you might do:
pal = StayPal.find_by(user_id: 1)
pal.places.people
That would get you all of the people that are in the places which user_id: 1 is in.
If you wanted to find all places which have associated people:
Place.as(:place1).people.places(:place2).pluck(:place1, :place2)
You could even count the number of people that exist in that relationship between places:
Place.as(:place1).people(:person).places(:place2).pluck(:place1, :place2, 'count(person)')

Related

Ruby On Rails - Coverting a JOIN using HAVING to a scope

I tried to ask this question previously and it didn't go well - hopefully I do it better this time.
I have three models
class Flavor < ActiveRecord::Base
has_many :components
has_many :ingredients, through: :components
end
class Ingredient < ActiveRecord::Base
has_many :components
has_many :flavors, through: :components
end
class Component < ActiveRecord::Base
belongs_to :ingredient
belongs_to :flavor
validates :percentage, presence: true
end
Batches are made of flavors, but a flavor can only be made into a batch if it's components add up to 100 percent (hence why I put the percentage validation in there so it was represented).
At first I tried to write this as a scope, but could never get it to work, the model testing i created worked using
def self.batch_eligible
self.find_by_sql("Select flavors.* FROM flavors
INNER JOIN components on flavors.id = components.flavor_id
GROUP BY flavors.id, flavors.name
HAVING SUM(percentage)=100")
end
I did make an attempt at the scope and it failed. Here is the final version of the scope I came up with:
scope :batch_eligible, -> {joins(:components).having('SUM(percentage) = 100').group('flavor.id')}
The resultant object will be used to populate a selection list in a form for batches (flavors can exist before the components are fully worked out).
I figure the limitation here is my understanding of scopes - so how would the scope be built properly to produce the same results as the find_by_sql expression?
All help is appreciated, thanks.
In response to the first comment - I tried a variety of scopes without capturing the errors - the scope above returns this error:
ActiveRecord::StatementInvalid:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "flavor"
LINE 1: SELECT COUNT(*) AS count_all, flavor.id AS flavor_id FROM "f...
^
: SELECT COUNT(*) AS count_all, flavor.id AS flavor_id FROM "flavors" INNER JOIN "components" ON "components"."flavor_id" = "flavors"."id" GROUP BY flavor.id HAVING SUM(percentage) = 100
changing it to flavors id makes it 'work' but it doesn't return the proper information.
One more piece of code - models testing
require 'rails_helper'
RSpec.describe Flavor, type: :model do
let!(:flavor) {FactoryGirl.create(:flavor)}
let!(:flavor2) {FactoryGirl.create(:flavor)}
let!(:ingredient) {FactoryGirl.create(:ingredient)}
let!(:component) {FactoryGirl.create(:component, flavor: flavor, ingredient: ingredient, percentage: 25)}
let!(:component1) {FactoryGirl.create(:component, flavor: flavor2, ingredient: ingredient, percentage: 100)}
it "should have a default archive as false" do
expect(flavor.archive).to be(false)
end
it "should only have valid flavors for batch creation" do
expect(Flavor.batch_eligible.count).to eq 1
expect(Flavor.batcH_eligible.first).to eq flavor2
end
end
Even with a clean test database - the batch_eligible count is 4 - not one
One more note - the tests DO pass with the find_by_sql function being used - I just think a scope should be possible?
Props to #taryneast for the help - I was pointed in the right direction by this.
After correcting the scope issue with flavors.id - I did run the inspect to see what was happening but I also ran a variety of functions.
puts Flavor.batch_eligible.count or puts Flavor.batch_eligible.size both yield the same thing, for example, the hash {312 => 1} - 312 would be the id of the Factory Created Flavor.
So the problem (once I solved flavors.id) wasn't in the scope - it was in the test. You need to test the LENGTH, Flavor.batch_eligible.length yields the integer 1 that I wanted.
Perhaps everyone else knew that - I didn't.
Thank you Taryn

MongoDB conditional aggregate query on a HABTM relationship (Mongoid, RoR)?

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.

Rails count linked tables

I have three objects that are linked to each other. It looks like this:
tree[
id: 1,
name: "name"
]
branch[
id: 1,
tree_id: 1
]
leaf[
id: 1,
branch_id:1
]
I need to know how many leaves belong to a every tree so I created a nested for loop:
update
I updated code:
results = []
trees = Tree.all
trees.each do |tree|
branches = Branch.where(tree_id: tree.id)
branches.each do |branch|
leaves_count = leaves.where(branch_id: branch.id).count
end
results.push( {
tree: tree.name,
leaves: leaves_count
})
end
with this code I receive an array of objects like so:
results = [{
tree: "oak",
leaves: 4
}, {
tree: "redwood",
leaves: 6
}]
I would like the same output but in a rails way
This works but feels realy clunky. Does anyone know a rails way of fixing this?
update solved
Yury Lebedev's answer is correct but through his previous answer I found another way. So this works (yury's answer):
Tree.find_each do |tree|
Branch.where(tree_id: tree.id).joins(:leaves).count('leaves.id')
end
This also works:
Tree.find_each do |tree|
Tree.joins(branches: :leaves).where(id: tree.id).count('leaves.id')
end
As a continuation question (not sure if I'm supposed to do that on answered questions?) I would like to know i one is better/faster then the other or whether they'r the same.
You can try it with a join query (this will only make one query to the db):
Leaf.joins('inner join branches on leaves.branch_id = branches.id')
.group('branches.tree_id').count
This will give you a hash with tree_id as a keys, and leaves count as values.
If you want to iterate through the trees, and get the leaves count for each of them, you can do this (this will make n queries to the db, if you have n trees):
trees.each do |tree|
leaves_count = Branch.where(tree_id: tree.id).joins('inner join leaves on leaves.branch_id = branches.id')
.count('leaves.id')
end
And this can be done even easier, if you have associations in your models:
class Three < ActiveRecord::Base
has_many :branches
has_many :trees, through: :branches
end
class Branch < ActiveRecord::Base
belongs_to :tree
has_many :leaves
end
class Leaf < ActiveRecord::Base
belongs_to :branch
delegate :tree, to: :branch
end
Then for a single query:
Leaf.joins(:branch).group('branches.tree_id').count
And for a query for each tree:
Tree.find_each do |tree|
Branch.where(tree_id: tree.id).joins(:leaves).count('leaves.id')
end

Rails 4. How show movies list where purchases is nil? [duplicate]

Consider a simple association...
class Person
has_many :friends
end
class Friend
belongs_to :person
end
What is the cleanest way to get all persons that have NO friends in ARel and/or meta_where?
And then what about a has_many :through version
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
end
class Friend
has_many :contacts
has_many :people, :through => :contacts, :uniq => true
end
class Contact
belongs_to :friend
belongs_to :person
end
I really don't want to use counter_cache - and I from what I've read it doesn't work with has_many :through
I don't want to pull all the person.friends records and loop through them in Ruby - I want to have a query/scope that I can use with the meta_search gem
I don't mind the performance cost of the queries
And the farther away from actual SQL the better...
Update 4 - Rails 6.1
Thanks to Tim Park for pointing out that in the upcoming 6.1 you can do this:
Person.where.missing(:contacts)
Thanks to the post he linked to too.
Update 3 - Rails 5
Thanks to #Anson for the excellent Rails 5 solution (give him some +1s for his answer below), you can use left_outer_joins to avoid loading the association:
Person.left_outer_joins(:contacts).where(contacts: { id: nil })
I've included it here so people will find it, but he deserves the +1s for this. Great addition!
Update 2
Someone asked about the inverse, friends with no people. As I commented below, this actually made me realize that the last field (above: the :person_id) doesn't actually have to be related to the model you're returning, it just has to be a field in the join table. They're all going to be nil so it can be any of them. This leads to a simpler solution to the above:
Person.includes(:contacts).where(contacts: { id: nil })
And then switching this to return the friends with no people becomes even simpler, you change only the class at the front:
Friend.includes(:contacts).where(contacts: { id: nil })
Update
Got a question about has_one in the comments, so just updating. The trick here is that includes() expects the name of the association but the where expects the name of the table. For a has_one the association will generally be expressed in the singular, so that changes, but the where() part stays as it is. So if a Person only has_one :contact then your statement would be:
Person.includes(:contact).where(contacts: { person_id: nil })
Original
Better:
Person.includes(:friends).where(friends: { person_id: nil })
For the hmt it's basically the same thing, you rely on the fact that a person with no friends will also have no contacts:
Person.includes(:contacts).where(contacts: { person_id: nil })
smathy has a good Rails 3 answer.
For Rails 5, you can use left_outer_joins to avoid loading the association.
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Check out the api docs. It was introduced in pull request #12071.
This is still pretty close to SQL, but it should get everyone with no friends in the first case:
Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Persons that have no friends
Person.includes(:friends).where("friends.person_id IS NULL")
Or that have at least one friend
Person.includes(:friends).where("friends.person_id IS NOT NULL")
You can do this with Arel by setting up scopes on Friend
class Friend
belongs_to :person
scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) }
scope :to_nobody, ->{ where arel_table[:person_id].eq(nil) }
end
And then, Persons who have at least one friend:
Person.includes(:friends).merge(Friend.to_somebody)
The friendless:
Person.includes(:friends).merge(Friend.to_nobody)
Both the answers from dmarkow and Unixmonkey get me what I need - Thank You!
I tried both out in my real app and got timings for them - Here are the two scopes:
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") }
scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") }
end
Ran this with a real app - small table with ~700 'Person' records - average of 5 runs
Unixmonkey's approach (:without_friends_v1) 813ms / query
dmarkow's approach (:without_friends_v2) 891ms / query (~ 10% slower)
But then it occurred to me that I don't need the call to DISTINCT()... I'm looking for Person records with NO Contacts - so they just need to be NOT IN the list of contact person_ids. So I tried this scope:
scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }
That gets the same result but with an average of 425 ms/call - nearly half the time...
Now you might need the DISTINCT in other similar queries - but for my case this seems to work fine.
Thanks for your help
Unfortunately, you're probably looking at a solution involving SQL, but you could set it in a scope and then just use that scope:
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0")
end
Then to get them, you can just do Person.without_friends, and you can also chain this with other Arel methods: Person.without_friends.order("name").limit(10)
A NOT EXISTS correlated subquery ought to be fast, particularly as the row count and ratio of child to parent records increases.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
Also, to filter out by one friend for instance:
Friend.where.not(id: other_friend.friends.pluck(:id))
Here is an option using a subquery:
# Scenario #1 - person <-> friend
people = Person.where.not(id: Friend.select(:person_id))
# Scenario #2 - person <-> contact <-> friend
people = Person.where.not(id: Contact.select(:person_id))
The above expressions should generate the following SQL:
-- Scenario #1 - person <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT friends.person_id
FROM friends
)
-- Scenario #2 - person <-> contact <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT contacts.person_id
FROM contacts
)

ActiveRecord group by on a join

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).

Resources