I think my question is best described as an example. Let's say I have a simple model called "Thing" and it has a few attributes that are simple data types. Something like...
Thing
- foo:string
- goo:string
- bar:int
That isn't hard. The db table will contain three columns with those three attributes and I can access them with something like #thing.foo or #thing.bar.
But the problem I'm trying to solve is what happens when "foo" or "goo" can no longer be contained in a simple data type? Assume that foo and goo represent the same type of object. That is, they are both instances of "Whazit" just with different data. So now Thing might look like this...
Thing
- bar:int
But now there is a new model called "Whazit" that looks like this...
Whazit
- content:string
- value:int
- thing_id:int
So far this is all good. Now here is where I'm stuck. If I have #thing, how can I set it up to refer to my 2 instances of Whazit by name (For the record, the "business rule" is that any Thing will always have exactly 2 Whazits)? That is, I need to know if the Whazit I have is basically foo or goo. Obviously, I can't do #thing.foo in the current setup, but I'd that is ideal.
My initial thought is to add a "name" attribute to Whazit so I can get the Whatzits associated with my #thing and then choose the Whazit I want by name that way. That seems ugly though.
Is there a better way?
There are a couple of ways you could do this. First, you could set up two belongs_to/has_one relationships:
things
- bar:int
- foo_id:int
- goo_id:int
whazits
- content:string
- value:int
class Thing < ActiveRecord::Base
belongs_to :foo, :class_name => "whazit"
belongs_to :goo, :class_name => "whazit"
end
class Whazit < ActiveRecord::Base
has_one :foo_owner, class_name => "thing", foreign_key => "foo_id"
has_one :goo_owner, class_name => "thing", foreign_key => "goo_id"
# Perhaps some before save logic to make sure that either foo_owner
# or goo_owner are non-nil, but not both.
end
Another option which is a little cleaner, but also more of a pain when dealing with plugins, etc., is single-table inheritance. In this case you have two classes, Foo and Goo, but they're both kept in the whazits table with a type column that distinguishes them.
things
- bar:int
whazits
- content:string
- value:int
- thing_id:int
- type:string
class Thing < ActiveRecord::Base
belongs_to :foo
belongs_to :goo
end
class Whazit < ActiveRecord::Base
# .. whatever methods they have in common ..
end
class Foo < Whazit
has_one :thing
end
class Goo < Whazit
has_one :thing
end
In both cases you can do things like #thing.foo and #thing.goo. With the first method, you'd need to do things like:
#thing.foo = Whazit.new
whereas with the second method you can do things like:
#thing.foo = Foo.new
STI has its own set of problems, though, especially if you're using older plugins and gems. Usually it's an issue with the code calling #object.class when what they really want is #object.base_class. It's easy enough to patch when necessary.
Your simple solution with adding a "name" doesn't need to be ugly:
class Thing < ActiveRecord::Base
has_one :foo, :class_name => "whazit", :conditions => { :name => "foo" }
has_one :goo, :class_name => "whazit", :conditions => { :name => "goo" }
end
In fact, it's quite similar to how STI works, except you don't need a separate class.
The only thing you'll need to watch out for is setting this name when you associate a whazit. That can be as simple as:
def foo=(assoc)
assos.name = 'foo'
super(assoc)
end
Related
I have a Gift model:
class Gift
include Mongoid::Document
include Mongoid::Timestamps
has_many :gift_units, :inverse_of => :gift
end
And I have a GiftUnit model:
class GiftUnit
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :gift, :inverse_of => :gift_units
end
Some of my gifts have gift_units, but others have not. How do I query for all the gifts where gift.gift_units.size > 0?
Fyi: Gift.where(:gift_units.exists => true) does not return anything.
That has_many is an assertion about the structure of GiftUnit, not the structure of Gift. When you say something like this:
class A
has_many :bs
end
you are saying that instance of B have an a_id field whose values are ids for A instances, i.e. for any b which is an instance of B, you can say A.find(b.a_id) and get an instance of A back.
MongoDB doesn't support JOINs so anything in a Gift.where has to be a Gift field. But your Gifts have no gift_units field so Gift.where(:gift_units.exists => true) will never give you anything.
You could probably use aggregation through GiftUnit to find what you're looking for but a counter cache on your belongs_to relation should work better. If you had this:
belongs_to :gift, :inverse_of => :gift_units, :counter_cache => true
then you would get a gift_units_count field in your Gifts and you could:
Gift.where(:gift_units_count.gt => 0)
to find what you're looking for. You might have to add the gift_units_count field to Gift yourself, I'm finding conflicting information about this but I'm told (by a reliable source) in the comments that Mongoid4 creates the field itself.
If you're adding the counter cache to existing documents then you'll have to use update_counters to initialize them before you can query on them.
I tried to find a solution for this problem several times already and always gave up. I just got an idea how this can be easily mimicked. It might not be a very scalable way, but it works for limited object counts. The key to this is a sentence from this documentation where it says:
Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.
So, get this done, you can define a class function like so:
def self.with_units
ids = Gift.all.select{|g| g.gift_units.count > 0}.map(&:id)
Gift.where(:id.in => ids)
end
The advantage is, that you can do all kinds of queries on the associated (GiftUnits) model and return those Gift instances, where those queries are satisfied (which was the case for me) and most importantly you can chain further queries like so:
Gift.with_units.where(:some_field => some_value)
I'm using Rails 2.3.14
UPDATE 3 the trick I found in update 2 only works for the first access of the association... so, I stick the set_table_name lines in my application controller. This is so weird. I hope this gets fixed. =\
UPDATE 2 if I manually / nastily / hackily set teh tables for the troublesome classes at the bottom of my environment file, I stop getting the errors.
app/config/environment.rb:
Page::Template::Content.set_table_name "template_page_contents"
Page::Template::Section.set_table_name "template_page_sections"
Why do I have to do this? is rails broken for this particular use case?
UPDATE: it appears that set_table_name isn't working when it's initially called in my Page::Template::Content class.
but if I call it right before I use the association, it works...
ap Page::Template::Content.table_name # prints "contents"
Page::Template::Content.set_table_name "template_page_contents"
ap Page::Template::Content.table_name # prints "template_page_contents"
return self.page_template_contents.first # doesn't error after the above is exec'd
ORIGINAL QUESTION:
TL; DR: I have a has_many relationship that I'm trying to access, but Rails thinks that the table that the has_many relationship uses is a different table
I've been getting this error, where when I try to access a Page::Template::Content that belongs_to a Page::Template via a has_many relationship.
Mysql2::Error: Unknown column 'contents.template_page_id' in 'where clause': SELECT * FROM `contents` WHERE (`contents`.template_page_id = 17) LIMIT 1
Looking at the error logs, I figured I needed to starting using some print statements to find out why rails was trying to find the associated objects in the wrong table.
gems_path/activerecord-2.3.14/lib/active_record/associations/association_collection.rb:63:in `find'
I just decided to print the #reflection object since everything seems to be happening around that. Here is how I did that:
require "awesome_print" # best console printer (colors, pretty print, etc)
def find(*args) # preexisting method header
ap "-------" # separator so I know when one reflection begins / ends, etc
ap #reflection # object in question
... # rest of the method
Last '#reflection' that printed before the error:
"-------"
#<ActiveRecord::Reflection::AssociationReflection:0x108d028a8
#collection = true,
attr_reader :active_record = class Page::Template < LibraryItem {...},
attr_reader :class_name = "Page::Template::Content",
attr_reader :klass = class Content < LibraryItem {...},
attr_reader :macro = :has_many,
attr_reader :name = :page_template_contents,
attr_reader :options = {
:foreign_key => "template_page_id",
:class_name => "Page::Template::Content",
:extend => []
},
attr_reader :primary_key_name = "template_page_id",
attr_reader :quoted_table_name = "`contents`"
>
there are a couple things wrong in the above block of code.
:klass should be Page::Template::Content
:name should be :contents
:quoted_table_name should be `contents`
How my models are set up:
app/models/page.rb:
class Page < LibrayItem
belongs_to :template_page, :class_name => "Page::Template"
app/models/page/template.rb
class Page::Template < Library Item
set_table_name "template_pages"
has_many :page_template_contents,
:class_name => "Page::Template::Content",
:foreign_key => "template_page_id"
app/models/page/template/content.rb
class Page::Template::Content
set_table_name "template_page_contents"
belongs_to :template_page,
:class_name => "Page::Template",
:foreign_key => "template_page_id"
class Page::Template
...
return self.page_template_contents.first
the class the association is choosing (unrelated to my page / template structure above):
class Content < LibraryItem
set_table_name "contents"
# no associations to above classes
So... what is causing this and how do I fix it?
Your issue is not due to set_table_name but rather due to how Rails finds the target model class in an association and the fact that you have a top-level model (Content) which shares its name with a model nested in a deeper namespace (Page::Template::Content). The weird difference in behaviour is most likely due to differences in what classes are actually loaded at the time the association is examined (remember that by default in development mode Rails loads model classes on demand when they are first referenced).
When you define an association (whether you specify the class name of the target model of the association explicitly as you have done or accept the default), Rails has to turn the name of the target model class into a Ruby constant so that it can refer to that target class. To do this, it invokes the protected class method compute_type on the model class that is defining the association.
For example, you have
class Page::Template < LibraryItem
set_table_name "template_pages"
has_many :page_template_contents,
:class_name => "Page::Template::Content",
:foreign_key => "template_page_id"
end
You can test in the Rails console how compute_type works for this class:
$ ./script/console
Loading development environment (Rails 2.3.14)
> Page::Template.send(:compute_type, "Page::Template::Content")
=> Page::Template::Content(id: integer, template_page_id: integer)
I see the result is, as I expect, a reference to the class Page::Template::Content.
However, if I restart the Rails console but this time cause the model class Content to be loaded first (by referencing it) and then try again, I see this (I didn't bother creating a table for the Content model, but that doesn't change the significant behavior):
$ ./script/console
Loading development environment (Rails 2.3.14)
>> Content # Reference Content class so that it gets loaded
=> Content(Table doesn't exist)
>> Page::Template.send(:compute_type, "Page::Template::Content")
=> Content(Table doesn't exist)
As you can see, this time I get a reference to Content instead.
So what can you do about this?
Well, first of all, you should realize that using namespaced model classes in Rails (certainly in 2.3) is a real pain. It's nice to organize your model classes in a hierarchy but it certainly does make life more difficult. As you can see above, if you have a class in one namespace that has the same name as a class in another namespace, this gets more hairy.
If you still want to live with this situation, I can make a couple of suggestions. One of the following may help:
Turn on class caching in development mode. This will cause all model classes to be preloaded rather than loading them on demand as is the default. It will make your code above more predictable, but may make development somewhat unpleasant (since classes will no longer be reloaded with each request).
Explicity require dependent classes in classes with problematic associations. For example:
class Page::Template < LibraryItem
require 'page/template/content'
set_table_name "template_pages"
has_many :page_template_contents, ...
end
Override the compute_type method in your classes where you know referencing associated class may go awry so that it returns the namespaced class no matter what. Without knowing your class hierachy in more detail I cannot give a full suggestion on how to do this, but a quick and dirty example follows:
class Page::Template < LibraryItem
set_table_name "template_pages"
has_many :page_template_contents,
:class_name => "Page::Template::Content",
:foreign_key => "template_page_id"
def self.compute_type(type_name)
return Page::Template::Content if type_name == "Page::Template::Content"
super
end
end
My task is to abstract/inherit an active record class. I'm making a blog where post is a base super with title, slug dates etc... all the redundant stuff you would expect to find.
Here's where things take a turn, I want to sub class out Post into many other sub post types such as audio post, video post, image post, vanilla post. I think you get the point. Obviously each sub type will have their own respective attributes and members.
Instead of creating a name, slug, etc., for each sub post type, what is the best practice to inherit or possibly interface the base class? ("I do favor composition over inheritance")
Once I figure out how to properly abstract out my models, I would like to then figure out some polymorphic way to say something like Blog.find(1).posts and get an array of all the posts types.
I realize that this may not be performance optimal to query all the post types in a polymorphic way so feel free to seguest a better way.
While I personally also prefer composition over inheritance, ActiveRecord does not. In this case, if you want to use tools that ActiveRecord offers, you should take a look at Single Table Inheritance, which would take care of both of your questions. It does use inheritance, however.
Switching to a non-ActiveRecord orm may offer you a way of doing this without having to do everything via inheritance. I've used DataMapper, which prefers composition, with success in the past, but it isn't as feature-packed as ActiveRecord and may not offer what you need.
Other then single table inheritance, you may also consider using has_one association.
All your sub-types has one post-info, which is the general post name, slug etc (and a post-info belongs to a sub-type polymorphically).
In this way, you would have a table of post-info, and tables for every sub-types.
However, in the model you have to do a little bit more handling:
class PostInfo < ActiveRecord::Base
belongs_to :post, :polymorphic => true
# will need these 2 fields: :post_id, :post_type (might be AudioPost, ImagePost, etc)
end
class AudioPost < ActiveRecord::Base
has_one :post_info, :as => :post
# you may also want these:
accept_nested_attributes_for :post_info
delegate :name, :slug, :posted_at, :to => :post_info
end
So now if you want to get all the posts, you may:
Blog.find(1).post_infos
post_info.post # => audio_post, image_post, or whatever depending on post_type
If you don't want to use .post_infos, you may also change all those names, such as:
class Post < ActiveRecord::Base
belongs_to :actual_post # actual_post_id, actual_post_type
end
class AudioPost < ActiveRecord::Base
has_one :post, :as => :actual_post
accept_nested_attributes_for :post
delegate :name, :slug, :posted_at, :to => :post
end
Now, you have:
posts = Blog.find(1).posts
actual_post = posts.first.actual_post # => an audio_post instance
actual_post.name # => same as actual_post.post.name, so you do not need the name field in the AudioPost model
I have a post model. Each post has a title and many snippets.
class Post < ActiveRecord::Base
has_many :snippets
end
class Snippet < ActiveRecord::Base
belongs_to :post
attr_accessible :position, :post_id
end
I want to have 4 different types of snippet i.e.:
body (text)
image (string)
code (text)
video (string)
Q1
Should I create four new models (called text, code, video and image) which extend the snippet model like so?:
class Text < Snipppet
attr_accessible :body
end
class Image < Snippet
attr_accessible :image
end
class Video < Snippet
attr_accessible :title
end
class Code < Snippet
attr_accessible code
end
Q2
How can I refer to the content of each snippet in my view when each snippet can be one of 4 different things?
In my view I'd like to put something like this:
- for snippet in #post.snippets
= place the content of the snippet here
Q3
I don't think it sounds like a good idea to have a "type" field on the snippet model as this would possibly lead to strong coupling of the database and the code. Is there some kind of rails magic that will help me out in this situation?
I kinda like the type field, actually. You could use the magic of single table inheritance, but it's well-known to be a fragile system that, surprise, includes a type field, anyway.
And then you could use the polymorphism solution, but seems a bit silly to have four different tables to represent almost exactly the same thing.
Honestly, using a simple type field and changing the interpretation based on that will probably give you the best results and fewest headaches.
As far as what you'd do in your template goes, that you can probably play by ear. One clean solution might be a simple helper to call like snippet_content which, based on the type field, will call the helper snippet_content_text or snippet_content_image or snippet_content_code or snippet_content_video. Or you can just do the if-then branching in the template, or refer to any number of partial templates (though be careful with that, since those can get slow when used unnecessarily).
I like this approach, but I always run into some subtle complications later on.
Create the appropriate views (texts/_text.html.haml, images/_image.html.haml, etc), so you can let Rails handle it for you:
= render #post.snippets
Like it or not: the "type"-field is the way to have Rails magic help you create the proper instances.
Update: polymorphic relations work too, let every snippet have their own tables. Is it a is a relation or a behaves like type of relation? You could say that Images and Texts both behave like snippets. In that case, go for a module named Snippet and mix it in.
class Post < ActiveRecord::Base
has_many :snippets, :polymorphic => true
end
class Text < ActiveRecord::Base
include Snippet
belongs_to :post, :as => :snippet
end
module Snippet
# shared behavior here
end
Many thanks to everyone who answered, but I think I've managed to fix this by using jystewart's rails 3 version of the acts_as_polymorphs gem
Here's what I did:
So let's recall, we have Posts and we have four different types of snippet. So that's 6 different models (post.rb, snippet.rb, code.rb. text.rb, image.rb and video.rb)
Posts have many snippets.
Snippets belong to posts.
Code, text, video and image are all types of snippet
And the big problem is that we don't know what type of object a snippet is, because it can be one of 4 different things.
At first, I tried to do this with Single Table Inheritance, but the objects (code, text, video and image) are, in my opinion, too different from one another for this to work well and I didn't like the fact that it would likely result in lots of empty database cells so I went along the Polymorphic route.
I couldn't get standard polymorphic associations to work here because this is not your usual polymorphic situation. Normally, when dealing with polymorphism, we're talking about something like a comments model, which can attach to multiple other models. The polymorphic entity is always the same thing. But in this case, we're talking about snippets model which can be one of 4 different things. This is not a simple belongs_to situation. Polymorphism was not happening.
Then I stumbled upon this article by m.onkey.org - which is a few years old but he basically explained that this sort of thing needs the acts_as_polymorphs gem.
So the solution was to do the following:
I create 6 models, all extending ActiveRecord::Base
add has_many_polymorphs to the post model
create a polymorphic association called "snippetable" in the snippet model
add some new fields to the snippets table through my migration file
Here's my code:
class Post < ActiveRecord::Base
has_many_polymorphs :snippets, :from => [:codes, :texts, :videos, :images], :through => :snippets
end
class Snippet < ActiveRecord::Base
belongs_to :snippetable, :polymorphic => true
end
class Code < ActiveRecord::Base
# don't have to put anything in here
end
class Text < ActiveRecord::Base
# don't have to put anything in here
end
class Video < ActiveRecord::Base
# don't have to put anything in here
end
class Image < ActiveRecord::Base
# don't have to put anything in here
end
The only other thing we need, is to stick a few new fields in the CreateSnippets migration:
class CreateSnippets < ActiveRecord::Migration
def self.up
create_table :snippets do |t|
t.references :post
t.column :snippetable_id, :integer, :null => false
t.column :snippetable_type, :string, :null => false
t.timestamps
end
end
end
And that's it! Unbelievably I can now go to rails console and do the following:
p = Post.first
p.codes << Code.create(:code=>"This is a code snippet")
p.images << Image.create(:image=>"xyz.png")
p.images << Image.create(:image=>"123.png")
p.codes.count # returns 1
p.images.count # returns 2
p.snippets.count # returns 3 !!!
Yaldi!
Anyway, it's taken me 11 days to fix this, and it REALLY depressed me that I couldn't do it. I hope this helps someone.
Here are some decent reading materials for acts_as_polymorph:
jstewart's rails 3 gem
Pratik Nait (of 37 signals) Blog post on it
What a carry on
I'm wondering what the easiest/most elegant way of selecting attributes from join models in has_many :through associations is.
Lets say we have Items, Catalogs, and CatalogItems with the following Item class:
class Item < ActiveRecord::Base
has_many :catalog_items
has_many :catalogs, :through => :catalog_items
end
Additionally, lets say that CatalogueItems has a position attribute and that there is only one CatalogueItem between any catalog and any item.
The most obvious but slightly frustrating way to retrieve the position attribute is:
#item = Item.find(4)
#catalog = #item.catalogs.first
#cat_item = #item.catalog_items.first(:conditions => {:catalog_id => #catalog.id})
position = #cat_item.position
This is annoying because it seems that we should be able to do #item.catalogs.first.position since we have completely specified which position we want: the one that corresponds to the first of #item's catalogs.
The only way I've found to get this is:
class Item < ActiveRecord::Base
has_many :catalog_items
has_many :catalogs, :through => :catalog_items, :select => "catalogue_items.position, catalogs.*"
end
Now I can do Item.catalogs.first.position. However, this seems like a bit of a hack - I'm adding an extra attribute onto a Catalog instance. It also opens up the possibility of trying to use a view in two different situations where I populate #catalogs with a Catalog.find or with a #item.catalogs. In one case, the position will be there, and in the other, it won't.
Does anyone have a good solution to this?
Thanks.
You can do something like this:
# which is basically same as your "frustrating way" of doing it
#item.catalog_items.find_by_catalogue_id(#item.catalogs.first.id).position
Or you can wrap it into in an instance method of the Item model:
def position_in_first_catalogue
self.catalog_items.find_by_catalogue_id(self.catalogs.first.id).position
end
and then just call it like this:
#item.position_in_first_catalogue
Just adding answer so that it might help others
CatalogItem.joins(:item, :catalog).
where(items: { id: 4 }).pluck(:position).first
You should be able to do #catalog.catalog_item.position if you provide the other end of the association.
class Catalog < ActiveRecord::Base
belongs_to :catalog_item
end
Now you can do Catalog.first.catalog_item.position.
Why don't You just
#item = Item.find(4)
position = #item.catalog_items.first.position
why do you go through catalogs? It doesn't make any sense to me since you are looking for first ANY catalog!?