Self referential embedded documents using Mongoid - ruby

Say I have a mongoid model called Foo that embeds many Bar.
class Foo
...
embeds_many :bar
...
end
class Bar
...
embedded_in :foo
...
end
I would like to create a relationship where Bar links to it's self. The relationship will always concern two documents that are embedded in the same Foo document. I don't seem to be able to do this with out getting nil back when calling the relationship. I have tried
belongs_to :discovered_by, :class_name => 'Bar'
and also
has_one :discovered_by, :class_name => 'Bar'
While the discovered_by id is set in the Bar document and pointing to the other Bar document when I try and do the following I get nil (assuming that the first Bar of the first Foo has the discovered_by_id set)
Foo.first.bars.first.discovered_by
This will always return nil despite the doucment having the id set. Any idea as to why this is happening? Thanks for any help.

You cannot have references to embedded models - even when they're both embedded in the same document. If you correctly configure the relationship
belongs_to :discovered_by, :class_name => 'Bar', inverse_of: :discovered
has_one :discovered, :class_name => 'Bar', inverse_of: :discovered_by
Mongoid will raise a Mongoid::Errors::MixedRelations exception. Maybe you could reconsider if embedding these objects is still the best choice. A workaround is storing only the id and query the parent object:
class Bar
include Mongoid::Document
embedded_in :foo
field :discovered_by_id, type: Moped::BSON::ObjectId
def discovered_by
foo.bars.find(discovered_by_id) if discovered_by_id
end
def discovered_by=(bar)
self.discovered_by_id = bar.id
end
end

Related

How can I set "global" variables that can be accessed in controllers and models in Rails

I have a table that has set entries. I would like to access those entries as variables in both my models and controllers without querying the database every time to set those variables.
I am able to get it to work by creating duplicate "concerns" for my models and controllers. I could also set global variables in my ApplicationController. Or i could initialize them in every place that I need them. What would be the correct rails way to set and access global variables that can be accessed in both controllers and models?
class ItemType
has_many :items
end
class Item
belongs_to :item_type
belongs_to :foo
end
class Foo
has_many :items
def build_item
bar_item_type = ItemType.find_by(:name => "bar")
self.items.build(
:foo_id => self.id,
:item_type_id => bar_item_type.id
)
end
end
class ItemsController
def update
bar_item_type = ItemType.find_by(:name => "bar")
#item.update(:item_type_id => bar_item_type.id)
end
end
In the example, you can see that I am declaring the bar_item_type variable in both my Foo model and my ItemsController. I would like to DRY up my code base by being able to create and access that variable once for my rails project instead of having to make that same database call everywhere.
I would advocate against such hard-coded or DB state-dependent code. If you must do it, here's how one of the ways I know it can be done:
# models
class ItemType < ActiveRecord::Base
has_many :items
# caches the value after first call
def self.with_bar
##with_bar ||= transaction { find_or_create_by(name: "bar") }
end
def self.with_bar_id
with_bar.id
end
end
class Item < ActiveRecord::Base
belongs_to :item_type
belongs_to :foo
scope :with_bar_types, -> { where(item_type_id: ItemType.with_bar_id) }
end
class Foo < ActiveRecord::Base
has_many :items
# automatically sets the foo_id, no need to mention explicitly
# the chained with_bar_types automatically sets the item_type_id to ItemType.with_bar_id
def build_item
self.items.with_bar_types.new
end
end
# Controller
class ItemsController
def update
#item.update(item_type_id: ItemType.with_bar_id)
end
end
If you MUST use a constant, there are a few ways to do it. But you must take into account that you are instantiating an ActiveRecord model object which is dependent on data being present in the database. This is not recommend, because you now have model and controller logic relying on data being present in the database. This might be ok if you have seeded your database and that it won't change.
class ItemType
BAR_TYPE ||= where(:name => "bar").limit(1).first
has_many :items
end
Now where ever you need this object you can call it like this:
bar_item_type = ItemType::BAR_TYPE

ActiveRecord association seems to read differently depending on reading forward or backward

I've read a ton of ActiveRecord SO Questions and haven't come across this yet. I know that the following code snippet is a little long winded but I'm not using Rails so I wanted to be clear about how my database was created/structured.
A household has the head (or heads) of household (like mom and dad), and it has children. Mom, Dad and the kids are all members of the household. I tried to implement that as follows. This is the full code snippet so you can just copy and paste and run it if you have active_record and sqlite3.
I wrote the question in-line in the code, but here it is in case you don't want to skim the code: when I do household.heads I get the members which I assigned as heads of household. But when I run member.household.heads (on the same household!) I don't get the heads, I get the kids! My only thought is that I shouldn't be using two 'has_many's with the same foreign_key, but everything else I've tried doesn't work.
require 'active_record'
ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'w'))
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => 'test.db'
)
ActiveRecord::Schema.define do
unless ActiveRecord::Base.connection.tables.include? 'members'
create_table :members do |table|
table.integer :household_id
table.integer :head_id
table.integer :child_id
table.string :name
end
end
unless ActiveRecord::Base.connection.tables.include? 'households'
create_table :households do |table|
table.string :address
end
end
end
class Member < ActiveRecord::Base
belongs_to :household
end
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :household_id
end
#Create some members
m1 = Member.create(name: "Foo")
m2 = Member.create(name: "Bar") #Foo's wife
m3 = Member.create(name: "foo-foo") #Foo and Bar's little girl
#Create a household
h1 = Household.create(address: "123 Fake St.")
#Assign members to households
h1.heads = [m1, m2]
h1.children = [m3]
#first let's check, h1 is m1's household. The two are the same.
p h1.id == m1.household.id
#So why doesn't this work?
h1.heads.each{|head| p head.name} #returns Foo and Bar
m1.household.heads.each{|head| p head.name} #<= Doesn't return Foo and Bar ?!?
h1.children.each{|child| p child.name} #returns foo-foo
m1.household.children.each{|child| p child.name} #Also returns foo-foo as expected
As it turns out, ActiveRecord is not OK with two Models of the same class being referred to by the same foreign key (household_id). You're just overwriting the assignment of h1's members when you assign children (or something like that, this still isn't clear to me).
You either need different classes: Head and Child or you need to be ok with the child calling the household differently.
I ended up changing the foreign keys to: household_id and house_id since I need heads.household more than I need child.household
(I still think it is annoying to use children.house but oh well!)
To be clear:
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :house_id
end

Mongoid model with hardcoded data

I have a mongoid model
class MyMongoidModel
include Mongoid::Document
include Mongoid::Timestamps
field :name, :type => String
field :data_id, :type => Integer
has_and_belongs_to_many :the_other_model, :class_name => 'class_name_model'
has_many :model2
def self.all
[
#.... the hardcoded data that will never be changed
]
end
end
it's used by the other model and it uses them as well. However, it contains the data that won't be changed for a very long time, let's say, at all. Thus, I don't want to retrieve it from db, I want it to be hardcoded and, at the same time, I want it acts like a normal mongoid model. Using caching is not what I'm looking for.
I hope you understand what I mean.
How do accomplish it?
There's a great gem called active_hash that provides this functionality for ActiveRecord: defining a fixed set of data as models you can reference/relate to normal models, but have it defined in code and loaded in memory (not stored/retrieved from DB).
https://github.com/zilkey/active_hash
Interestingly, since Mongoid and ActiveRecord both share common ActiveModel basis, you may be able to use active_hash with a Mongoid document now.
For example:
class Country < ActiveHash::Base
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
class Order
include Mongoid::Document
include Mongoid::Timestamps
has_one :country
end

How to have an ordered model association allowing duplicates

I have two models, Song and Show. A Show is an ordered list of Songs, in which the same Song can be listed multiple times.
That is, there should be an ordered array (or hash or anything) somewhere in Show that can contain Song1, Song2, Song1, Song3 and allow re-ordering, inserting, or deleting from that array.
I cannot figure out how to model this with ActiveRecord associations. I'm guessing I need some sort of special join table with a column for the index, but apart from starting to code my SQL directly, is there a way to do this with Rails associations?
Some code as I have it now (but doesn't work properly):
class Song < ActiveRecord::Base
attr_accessible :title
has_and_belongs_to_many :shows
end
class Show < ActiveRecord::Base
attr_accessible :date
has_and_belongs_to_many :songs
end
song1 = Song.create(title: 'Foo')
song2 = Song.create(title: 'Bar')
show1 = Show.create(date: 'Tomorrow')
show1.songs << song1 << song2 << song1
puts "show1 size = #{show1.songs.size}" # 3
show1.delete_at(0) # Should delete the first instance of song1, but leave the second instance
puts "show1 size = #{show1.songs.size}" # 2
show1.reload
puts "show1 size = #{show1.songs.size}" # 3 again, annoyingly
Inserting might look like:
show1.songs # Foo, Bar, Foo
song3 = Song.create(title: 'Baz')
show1.insert(1, song3)
show1.songs # Foo, Baz, Bar, Foo
And reordering might (with a little magic) look something like:
show1.songs # Foo, Bar, Foo
show1.move_song_from(0, to: 1)
show1.songs # Bar, Foo, Foo
You're on the right track with the join table idea:
class Song < ActiveRecord::Base
attr_accessible :title
has_many :playlist_items
has_many :shows, :through => :playlist_items
end
class PlaylistItem < ActiveRecord::Base
belongs_to :shows #foreign_key show_id
belongs_to :songs #foreign_key song_id
end
class Show < ActiveRecord::Base
attr_accessible :date
has_many :playlist_items
has_many :songs, :through => :playlist_items
end
Then you can do stuff like user.playlist_items.create :song => Song.last
My current solution to this is a combination of has_many :through and acts_as_list. It was not the easiest thing to find information on combining the two correctly. One of the hurdles, for example, was that acts_as_list uses an index starting at 1, while the array-like methods created by the ActiveRecord association start at 0.
Here's how my code ended up. Note that I had to specify explicit methods to modify the join table (for most of them anyway); I'm not sure if there's a cleaner way to make those work.
class Song < ActiveRecord::Base
attr_accessible :title
has_many :playlist_items, :order => :position
has_many :shows, :through => :playlist_items
end
class PlaylistItem < ActiveRecord::Base
attr_accessible :position, :show_id, :song_id
belongs_to :shows
belongs_to :songs
acts_as_list :scope => :show
end
class Show < ActiveRecord::Base
attr_accessible :date
has_many :playlist_items, :order => :position
has_many :songs, :through => :playlist_items, :order => :position
def song_at(index)
self.songs.find_by_id(self.playlist_items[index].song_id)
end
def move_song(index, options={})
raise "A :to option is required." unless options.has_key? :to
self.playlist_items[index].insert_at(options[:to] + 1) # Compensate for acts_as_list starting at 1
end
def add_song(location)
self.songs << location
end
def remove_song_at(index)
self.playlist_items.delete(self.playlist_items[index])
end
end
I added a 'position' column to my 'playlist_items' table, as per the instructions that came with acts_as_list. It's worth noting that I had to dig into the API for acts_as_list to find the insert_at method.

Mongoid Relations 1..*

Consider the following:
class Picture
include Mongoid::Document
field :data, :type => String
end
class Cat
include Mongoid::Document
has_one :picture, :autosave => true
field :name, :type => String
end
class Dog
include Mongoid::Document
has_one :picture, :autosave => true
field :name, :type => String
end
Now, is it possible to do the following:
dog = Dog.new
dog.picture = Picture.new
dog.save!
Without having to edit the Picture class to the following:
class Picture
include Mongoid::Document
belongs_to :cat
belongs_to :dog
field :data, :type => String
end
I don't need pictures to know about it's Dog or Cat. Is this possible?
I believe you could do this if you put the belongs_to :picture in your dog and cat classes. The side of the relation that has belongs_to is the side that will store the foreign key. That would put a picture_id field in each of Dog and Cat, instead of having to store a whatever_id for each type of think you want to link on your Picture class.
No it is not. You need to have cat_id or dog_id or some polymorphic obj_id for all of them to store information about belonging of this picture.
Or how do you know wich Picture belongs to current Dog or Cat?

Resources