I have a Mongo collection that simply references an ID to another collection. Hypothetically the collection I'm specifically referring to might be called:
Walks. Walks has a reference to owner_id. The owner takes many walks with his many pets every day. What I want to do is query Walks for a list of N owner_ids and get only the last walk they took for each owner and group that by owner_id. To get a list of all walks by said list we'd do something like.
Walk.any_in(:owner_id => list_of_ids)
My question is, is there a way to query that list_of_ids, get only one walk per owner_id (the last one they took which can be sorted by the field created_at and returned in a hash where each walk is pointed to by an owner_id such as:
{ 5 => {..walk data..}, 10 => {.. walk data ..}}
Here's an answer that uses MongoDB's group command.
For the purposes of testing, I've used walk_time instead of created_at.
Hope that this helps and that you like it.
class Owner
include Mongoid::Document
field :name, type: String
has_many :walks
end
class Walk
include Mongoid::Document
field :pet_name, type: String
field :walk_time, type: Time
belongs_to :owner
end
test/unit/walk_test.rb
require 'test_helper'
class WalkTest < ActiveSupport::TestCase
def setup
Owner.delete_all
Walk.delete_all
end
test "group in Ruby" do
walks_input = {
'George' => [ ['Fido', 2.days.ago], ['Fifi', 1.day.ago], ['Fozzy', 3.days.ago] ],
'Helen' => [ ['Gerty', 4.days.ago], ['Gilly', 2.days.ago], ['Garfield', 3.days.ago] ],
'Ivan' => [ ['Happy', 2.days.ago], ['Harry', 6.days.ago], ['Hipster', 4.days.ago] ]
}
owners = walks_input.map do |owner_name, pet_walks|
owner = Owner.create(name: owner_name)
pet_walks.each do |pet_name, time|
owner.walks << Walk.create(pet_name: pet_name, walk_time: time)
end
owner
end
assert_equal(3, Owner.count)
assert_equal(9, Walk.count)
condition = { owner_id: { '$in' => owners[0..1].map(&:id) } } # don't use all owners for testing
reduce = <<-EOS
function(doc, out) {
if (out.last_walk == undefined || out.last_walk.walk_time < doc.walk_time)
out.last_walk = doc;
}
EOS
last_walk_via_group = Walk.collection.group(key: :owner_id, cond: condition, initial: {}, reduce: reduce)
p last_walk_via_group.collect{|r|[Owner.find(r['owner_id']).name, r['last_walk']['pet_name']]}
last_walk = last_walk_via_group.collect{|r|Walk.new(r['last_walk'])}
p last_walk
end
end
test output
Run options: --name=test_group_in_Ruby
# Running tests:
[["George", "Fifi"], ["Helen", "Gilly"]]
[#<Walk _id: 4fbfa7a97f11ba53b3000003, _type: nil, pet_name: "Fifi", walk_time: 2012-05-24 15:39:21 UTC, owner_id: BSON::ObjectId('4fbfa7a97f11ba53b3000001')>, #<Walk _id: 4fbfa7a97f11ba53b3000007, _type: nil, pet_name: "Gilly", walk_time: 2012-05-23 15:39:21 UTC, owner_id: BSON::ObjectId('4fbfa7a97f11ba53b3000005')>]
.
Finished tests in 0.051868s, 19.2797 tests/s, 38.5594 assertions/s.
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
Related
I'm trying to write my test to ensure creating a new book with genres assigned to it works.
I am using Active Model Serializer with the JSON_API structure (http://jsonapi.org/)
Book Model File
class Book < ApplicationRecord
belongs_to :author, class_name: "User"
has_and_belongs_to_many :genres
end
Genre Model File
class Genre < ApplicationRecord
has_and_belongs_to_many :books
end
Book Serializer file
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :adult_content, :published
belongs_to :author
has_many :genres
end
Test Sample Data
def setup
...
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genres: [{title: 'Fantasy'}]
}
end
Test Method
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
puts "json = #{json}"
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre_data = json['data']['relationships']['genres']['data']
puts "genre_data = #{genre_data.count}"
assert_equal "Fantasy", genre_data
end
Book Strong Params
def book_params
params.permit(:title, :adult_content, :published, :author_id, :genres)
end
Test Result (console response)
# Running:
......................................................json = {"data"=>{"id"=>"1018350796", "type"=>"books", "attributes"=>{"title"=>"Three Little Pigs", "adult-content"=>false, "published"=>false}, "relationships"=>{"author"=>{"data"=>{"id"=>"1027431151", "type"=>"users"}}, "genres"=>{"data"=>[]}}}}
genre_data = 0
F
Failure:
BooksControllerTest#test_book_create_-_should_create_a_new_book [/Users/warlock/App_Projects/Raven Quill/Source Code/Rails/raven-quill-api/test/controllers/books_controller_test.rb:60]:
Expected: "Fantasy"
Actual: []
bin/rails test test/controllers/books_controller_test.rb:51
Finished in 1.071044s, 51.3518 runs/s, 65.3568 assertions/s.
55 runs, 70 assertions, 1 failures, 0 errors, 0 skips
As you can see from my JSON console log, it appears my genres are not being set(need to scroll to the right in the test output above).
Please ignore this line:
assert_equal "Fantasy", genre_data
I know that's wrong. At the moment, the json is showing genre => {data: []} (empty array), that's the thing I'm trying to solve at the moment.
How do I go about creating a book with genres in this case, any ideas? :D
This is just sad...third time this week, I am answering my own question.
I finally found the answer from this Stackoverflow question:
HABTM association with Strong Parameters is not saving user in Rails 4
Turns out my strong parameters need to be:
def book_params
params.permit(:title, :adult_content, :published, :author_id, {:genre_ids => []})
end
Then my test data can be:
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genre_ids: [#fantasy.id]
}
Update my test method to:
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre = json['data']['relationships']['genres']['data'].first['title']
assert_equal "Fantasy", genre
end
Now my test passes.
Lets set the stage for this question.
Here is our model:
class Deal < ActiveRecord::Base
belongs_to :retailer
has_many :content_locations, as: :locatable
has_many :stores, through: :content_locations
searchable do
text :title
text :store_name
text :store_sort_name
text :description
end
scope :by_keyword, ->(keyword, limit, offset) {
search {
fulltext keyword
paginate offset: offset, per_page: limit
}.results
}
end
Here is the section of our rspec test that hits SOLR:
describe 'searchable' do
before do
DatabaseCleaner.clean
end
target = 'foo'
let!(:deal_with_title) { create :deal, title: target }
let!(:deal_with_description) { create :deal, description: target }
let!(:deal_with_store_name) { create :deal, store_name: target }
let!(:deal_with_store_sort_name) { create :deal, store_sort_name: target }
let!(:deal_with_nothing) {
create :deal, title: 'bar', description: 'bar', store_name: 'bar', store_sort_name: 'bar'
}
it 'includes matches' do
sleep 1
results = Deal.by_keyword(target, 25, 0)
expect(results).to include(deal_with_title)
expect(results).to include(deal_with_description)
expect(results).to include(deal_with_store_name)
expect(results).to include(deal_with_store_sort_name)
expect(results).to_not include(deal_with_nothing)
end
end
What we are seeing is that our test fails randomly. I can execute this test over and over successfully for 1-3xs but the 4th will fail, or it will fail 4xs and the 5th will fail. The pattern is completely random. As you see we tried adding a sleep to this setup and that doesn't do anything but slow it down.
Any tips would be greatly appreciated here. This has been driving us nuts!
In the Sunspot Model Spec they're allways calling
Sunspot.commit
after data creation and before any check
Update:
You can also call model.index!, which will immediately sync the particular model into solr index. In one specific case i also had to call model.reload after saving and before indexing, because of some tag relationships attached to the model (however this was acts_as_taggable specific)
#simple
Factory.create(:model).tap { |m| m.index! }
#advanced
model = Factory.build :model_with_relationships
model.save
model.reload #optional
model.index!
We decided to go a completely different direction and honestly I would not recommend hitting SOLR in real time for anyone reading this post. It is a pain.
Our final solution was to with the sunspot_matchers gem instead.
This is rife with problems and unlike anything I've witnessed with SQL. I'm simply trying to create an HABTM relationship and make the objects match each other.
My Two Models
class Word
include MongoMapper::Document
many :topics
key :word, String
end
class Topic
include MongoMapper::Document
many :words
key :name, String
end
That model work alone allows me to create objects and associate them. Part of why I love Mongo.
Then I try to take some sample yaml like so :
Music I Dig:
reggae:
bob marley
isaac hayes
groundation
classical:
philip glass
bach
And trying to parse it with this Rakefile :
File.open(Rails.root + 'lib/words/disgusting_glass_full_of.yml', 'r') do |file|
YAML::load(file).each do |topic, word_types|
puts "Adding #{topic}.."
#temp_topic = Topic.create name: topic
#temp_words = word_types.map { |type, words|
words.split(' ').map{ |word|
#word = Word.create type: type, word: word
#word.topics << #temp_topic
}
}
#temp_topic.words << #temp_words.flatten
end
end
I kid you not, this is the most random, jangled output I've ever seen. 2 the amount of actual topics are created that are empty and have no data. Some topics have associations, some done. Same with words. Some words will randomly have associations, and other won't. I can't find any connection at all as to how its deriving this outcome.
I believe the problem comes with how I'm setting up my models ( maybe? ). If not, I'm throwing mongo_mapper and trying Mongoid.
Firstly, you'll need to specify that word_ids and topic_ids are array attributes in your models:
class Topic
include MongoMapper::Document
many :words, :in => :word_ids
key :word_ids, Array
key :name, String
end
class Word
include MongoMapper::Document
many :topics, :in => :topic_ids
key :topic_ids, Array
key :word, String
end
You'll also have to make sure that you are saving your topic and word in your rake task:
task :import => :environment do
File.open(Rails.root + 'lib/test.yml', 'r') do |file|
YAML::load(file).each do |topic, word_types|
puts "Adding #{topic}.."
temp_topic = Topic.create name: topic
temp_words = []
word_types.map do |type, words|
words.split(' ').map do |word|
word = Word.create type: type, word: word
word.topics << temp_topic
word.save
temp_words << word
end
end
temp_topic.words << temp_words.flatten
temp_topic.save
end
end
end
That gives me the following output:
{
"_id" : ObjectId("502bc54a3005c83a3a000006"),
"topic_ids" : [
ObjectId("502bc54a3005c83a3a000001")
],
"word" : "groundation",
"type" : "reggae"
}
{
"_id" : ObjectId("502bc54a3005c83a3a000007"),
"topic_ids" : [
ObjectId("502bc54a3005c83a3a000001")
],
"word" : "philip",
"type" : "classical"
} ....etc
and
{
"_id" : ObjectId("502bc54a3005c83a3a000001"),
"word_ids" : [
ObjectId("502bc54a3005c83a3a000002"),
ObjectId("502bc54a3005c83a3a000003"),
ObjectId("502bc54a3005c83a3a000004"),
ObjectId("502bc54a3005c83a3a000005"),
ObjectId("502bc54a3005c83a3a000006"),
ObjectId("502bc54a3005c83a3a000007"),
ObjectId("502bc54a3005c83a3a000008"),
ObjectId("502bc54a3005c83a3a000009")
],
"name" : "Music I Dig"
}
Let's say I have a devise model called "User" which has_many :notes and :notebooks and each :notebook has_many :notes.
So a notes will have two foreign key, :user_id and :notebook_id, so how to build/find a note?
current_user.notebooks.find(param).notes.new(params[:item]) will create the foreign_key only for notebook or also for the user in the note record in the DB?
If the second case (foreign key only for notebook), how should I write?
Using MongoDB with MongoID and referenced relations
Mongoid will manage the document references and queries for you, just make sure to specify the association/relationship for each direction that you need (e.g., User has_many :notes AND Note belongs_to :user). Like ActiveRecord, it appears to be "smart" about the relations. Please do not manipulate the references manually, but instead let your ODM (Mongoid) work for you. As you run your tests (or use the rails console), you can tail -f log/test.log (or log/development.log) to see what MongoDB operations are being done by Mongoid for you and you can see the actual object references as the documents are updated. You can see how a relationship makes use of a particular object reference, and if you pay attention to this, the link optimization should become clearer.
The following models and test work for me. Details on the setup are available on request. Hope that this helps.
Models
class User
include Mongoid::Document
field :name
has_many :notebooks
has_many :notes
end
class Note
include Mongoid::Document
field :text
belongs_to :user
belongs_to :notebook
end
class Notebook
include Mongoid::Document
belongs_to :user
has_many :notes
end
Test
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
User.delete_all
Note.delete_all
Notebook.delete_all
end
test "user" do
user = User.create!(name: 'Charles Dickens')
note = Note.create!(text: 'It was the best of times')
notebook = Notebook.create!(title: 'Revolutionary France')
user.notes << note
assert_equal(1, user.notes.count)
user.notebooks << notebook
assert_equal(1, user.notebooks.count)
notebook.notes << note
assert_equal(1, notebook.notes.count)
puts "user notes: " + user.notes.inspect
puts "user notebooks: " + user.notebooks.inspect
puts "user notebooks notes: " + user.notebooks.collect{|notebook|notebook.notes}.inspect
puts "note user: " + note.user.inspect
puts "note notebook: " + note.notebook.inspect
puts "notebook user: " + notebook.user.inspect
end
end
Result
Run options: --name=test_user
# Running tests:
user notes: [#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]
user notebooks: [#<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">]
user notebooks notes: [[#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]]
note user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
note notebook: #<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">
notebook user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
.
Finished tests in 0.018622s, 53.6999 tests/s, 161.0998 assertions/s.
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
I would use
class User
has_many :notebooks
has_many :notes, :through => :notebooks
end
http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
Update
You could always set the user_id manually, like this (I assume param is the ID for your notebook?):
Notebook.find(param).notes.new(params[:item].merge(:user_id => current_user.id))
I am reading the book Simply Rails by Sitepoint and given these models:
story.rb
class Story < ActiveRecord::Base
validates_presence_of :name, :link
has_many :votes do
def latest
find :all, :order => 'id DESC', :limit => 3
end
end
def to_param
"#{id}-#{name.gsub(/\W/, '-').downcase}"
end
end
vote.rb
class Vote < ActiveRecord::Base
belongs_to :story
end
and given this fixtures
stories.yml
one:
name: MyString
link: MyString
two:
name: MyString2
link: MyString2
votes.yml
one:
story: one
two:
story: one
these tests fail:
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:one),votes(:two)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:two), stories(:one).votes.latest.first
end
however, if I reverse the order of the stories, for the first assertion and provide the first vote for the first assertion, it passes
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:two),votes(:one)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:one), stories(:one).votes.latest.first
end
I copied everything as it is in the book and have not seen an errata about this. My first conclusion was that the fixture is creating the records from bottom to top as it was declared, but that doesn't make any point
any ideas?
EDIT: I am using Rails 2.9 running in an RVM
Your fixtures aren't getting IDs 1, 2, 3, etc. like you'd expect - when you add fixtures, they get IDs based (I think) on a hash of the table name and the fixture name. To us humans, they just look like random numbers.
Rails does this so you can refer to other fixtures by name easily. For example, the fixtures
#parents.yml
vladimir:
name: Vladimir Ilyich Lenin
#children.yml
joseph:
name: Joseph Vissarionovich Stalin
parent: vladimir
actually show up in your database like
#parents.yml
vladimir:
id: <%= fixture_hash('parents', 'vladimir') %>
name: Vladimir Ilyich Lenin
#children.yml
joseph:
id: <%= fixture_hash('children', 'joseph') %>
name: Joseph Vissarionovich Stalin
parent_id: <%= fixture_hash('parents', 'vladimir') %>
Note in particular the expansion from parent: vladimir to parent_id: <%= ... %> in the child model - this is how Rails handles relations between fixtures.
Moral of the story: Don't count on your fixtures being in any particular order, and don't count on :order => :id giving you meaningful results in tests. Use results.member? objX repeatedly instead of results == [obj1, obj2, ...]. And if you need fixed IDs, hard-code them in yourself.
Hope this helps!
PS: Lenin and Stalin weren't actually related.
Xavier Holt already gave the main answer, but wanted to also mention that it is possible to force rails to read in fixtures in a certain order.
By default rails assigns its own IDs, but you can leverage the YAML omap specification to specify an ordered mapping
# countries.yml
--- !omap
- netherlands:
id: 1
title: Kingdom of Netherlands
- canada:
id: 2
title: Canada
Since you are forcing the order, you have to also specify the ID yourself manually, as shown above.
Also I'm not sure about this part, but I think once you commit to overriding the default rails generated ID and use your own, you have to do the same for all downstream references.
In the above example, suppose each country can have multiple leaders, you would have do something like
# leaders.yml
netherlands-leader:
country_id: 1 #you have to specify this now!
name: Willem-Alexander
You need to manually specify the id that refers to the previous Model (Countries)