I'm having a hard time understanding how to implement a single model self-join in Rails.
The Guide to ActiveRecord Associations section 2.10 briefly explains Self-Joins but doesn't offer enough information, and every example or post about this such references the Friendly Friend Railcast example that isn't a single model self join, as described in the Rails Guide section 2.10.
The idea is a model that has_many and belongs_to itself, without needing a separate table for the relationship. The only reason I see for needing a separate table is if you want the relationship to contain more information than just the relationship. e.g. "best friends", "barely know them"
I have a simple Post schema:
create_table "posts", :force => true do |t|
t.datetime "posted"
t.string "nick"
t.string "title"
t.text "content"
t.integer "parent_post_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
The parent_post_id is a self-reference to other Post post_id's. The posts.rb model has the relationship defined:
class Post < ActiveRecord::Base
has_many :replies, :class_name => "Post"
belongs_to :parent_post, :class_name => "Post",
:foreign_key => "parent_post_id"
end
In the Controller or View I'm hoping to be able to do something like this:
#posts.each do |post|
...
#replies = post.replies
#replies.each do |reply|
...
end
end
Or find a post's parent:
#parent_post = post.parent_post
This may all be some syntax mis-understanding. So thanks in advance to anyone who can slap some sense into me. I've looked through every SO and blog post out there and none try the single model self-referential self-join method described in the Guide.
Points for anyone offering an explanation that doesn't point to the Friendly Friend example that uses a separate relationship table.
I was missing the has_many foreign key to "parent_post_id". Once its set, the post.replies will reference other Post instances by parent_post_id.
The posts.rb model has the relationship defined:
class Post < ActiveRecord::Base
has_many :replies, :class_name => "Post",
:foreign_key => "parent_post_id"
belongs_to :parent_post, :class_name => "Post",
:foreign_key => "parent_post_id"
end
I can now create Posts, assign a parent_post_id to a different Post, then later get all Posts that are replies to any parent Post.
Related
I have a custom Primary Key (PK), ASIN, in my Products table. This PK is a string.
def change
create_table :products, id: false do |t|
t.references :user, null: false, foreign_key: true
t.primary_key :asin
t.string :image
t.string :price
t.string :title
t.timestamps
end
change_column :products, :asin, :string
end
In my models for Product, I have the following:
self.primary_key = 'asin'
belongs_to :user
validates_uniqueness_of :asin
validates_presence_of :asin, :image, :title, :price, :user_id
before_save :before_save
def before_save
self.asin = self.asin.upcase!
end
I have looked over the code several times but while trying to hit this Route and create a Product, this is the error I receive.
"exception": "#<ActiveRecord::NotNullViolation: SQLite3::ConstraintException: NOT NULL constraint failed: products.asin>"
And this is the JSON that I am sending to this create Route
{
"asin": "B",
"title": "Test",
"image": "test",
"user_id": 1,
"price": "test"
}
NOTE: I know that it is generally looked down upon to use PKs in this manor and please know I recognize this but this is the ideal setup for my project's goal. Please refrain from leaving mean-spirited comments.
EDIT: For reference, here is the Create function under the Products controller.
skip_before_action :authorize_request, only: [:create, :show]
def create
Product.create!(product_params)
json_response(product_params, :created)
end
def product_params
params.permit(
:asin,
:image,
:title,
:user_id,
:price
)
end
EDIT: Part of the issue was that the asin's first letter needed to be downcased before sending to the backend.
I am not totally sure what happened here to fix this. But I ran the following commands just to test things as I was sure this database setup was perfectly fine.
First, I stopped the server using control + c on mac.
I connected to rails using rails c
I connected to the products table using Product.connection
And I created a test Product just to see what was wrong Product.create!(asin: "Test_ASIN", title: "title", image: "image", price: "price", user_id: 1)
I was surprised to see that this worked since this is exactly what I sent to the backend originally, but then I went ahead and started rails s and tried to send the request again and this time, it submitted.
I hope this helps someone out there as this was obnoxiously weird.
I have a Model called Challenge that is created by a User. It has a Difficulty associated with it as well. When I create the Challenge and hand it the Author (User) and Difficulty, the Difficulty associate works, but the Author (User) one doesn't. The weirdest part is, when you look at the Challenge object, it shows the Author key its associated with.
challenge = Challenge.first
challenge.author (prints Nil) #Doesn't even run a Query
When I create a Challenge using the following code, the user_id is Nil.
user = User.find(1)
diff = Difficulty.find(1)
Challenge.create(:author => user, :difficulty => diff, :title => "My Challenge")
When I create a Challenge using this code, the User gets the relation to the Challenge and the Challenge gets the user_id of the User. But you can only go from User to Challenge. Challenge to User returns Nil.
user = User.find(1)
diff = Difficulty.find(1)
chall = Challenge.create(:difficulty => diff, :title => "My Challenge")
user.authored_challenges << chall
Here are my Models and Tables
# Models
class User < ActiveRecord::Base
has_many :authored_challenges, :class_name => "Challenge"
attr_accessible :display_name, :authored_challenges
end
class Reward < ActiveRecord::Base
has_many :challenges
attr_accessible :name, :value, :challenges
end
class Challenge < ActiveRecord::Base
belongs_to :reward
belongs_to :author, :class_name => "User"
attr_accessible :title, :description, :reward, :author
end
# Tables
create_table :users do |t|
t.string :display_name
t.timestamps
end
create_table :rewards do |t|
t.string :name
t.float :value
t.timestamps
end
create_table :challenges do |t|
t.integer :user_id
t.integer :reward_id
t.string :title
t.string :description
t.timestamps
end
According to: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html the foreign key should be:
:foreign_key => :author_id
Don't forget to add the field. Good Luck!
Have you tried:
belongs_to :author, :class_name => "User", :foreign_key => :user_id
From the Rails documentation:
By convention, Rails assumes that the column used to hold the foreign key on this model is the name of the association with the suffix _id added. The :foreign_key option lets you set the name of the foreign key directly
The example given in the docs is very similar to the one you have:
class Order < ActiveRecord::Base
belongs_to :customer, :class_name => "Patron", :foreign_key => "patron_id"
end
I've search everywhere for a pointer to this, but can't find one. Basically, I want to do what everyone else wants to do when they create a polymorphic relationship in a :has_many, :through way… but I want to do it in a module. I keep getting stuck and think I must be overlooking something simple.
To wit:
module ActsPermissive
module PermissiveUser
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_permissive
has_many :ownables
has_many :owned_circles, :through => :ownables
end
end
end
class PermissiveCircle < ActiveRecord::Base
belongs_to :ownable, :polymorphic => true
end
end
With a migration that looks like this:
create_table :permissive_circles do |t|
t.string :ownable_type
t.integer :ownable_id
t.timestamps
end
The idea, of course, is that whatever loads acts_permissive will be able to have a list of circles that it owns.
For simple tests, I have
it "should have a list of circles" do
user = Factory :user
user.owned_circles.should be_an_instance_of Array
end
which fails with:
Failure/Error: #user.circles.should be_an_instance_of Array
NameError: uninitialized constant User::Ownable
I've tried: using :class_name => 'ActsPermissive::PermissiveCircle' on the has_many :ownables line, which fails with:
Failure/Error: #user.circles.should be_an_instance_of Array
ActiveRecord::HasManyThroughSourceAssociationNotFoundError:
Could not find the source association(s) :owned_circle or
:owned_circles in model ActsPermissive::PermissiveCircle.
Try 'has_many :owned_circles, :through => :ownables,
:source => <name>'. Is it one of :ownable?
while following the suggestion and setting :source => :ownable fails with
Failure/Error: #user.circles.should be_an_instance_of Array
ActiveRecord::HasManyThroughAssociationPolymorphicSourceError:
Cannot have a has_many :through association 'User#owned_circles'
on the polymorphic object 'Ownable#ownable'
Which seems to suggest that doing things with a non-polymorphic-through is necessary. So I added a circle_owner class similar to the setup here:
module ActsPermissive
class CircleOwner < ActiveRecord::Base
belongs_to :permissive_circle
belongs_to :ownable, :polymorphic => true
end
module PermissiveUser
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_permissive
has_many :circle_owners, :as => :ownable
has_many :circles, :through => :circle_owners,
:source => :ownable,
:class_name => 'ActsPermissive::PermissiveCircle'
end
end
class PermissiveCircle < ActiveRecord::Base
has_many :circle_owners
end
end
With a migration:
create_table :permissive_circles do |t|
t.string :name
t.string :guid
t.timestamps
end
create_table :circle_owner do |t|
t.string :ownable_type
t.string :ownable_id
t.integer :permissive_circle_id
end
which still fails with:
Failure/Error: #user.circles.should be_an_instance_of Array
NameError:
uninitialized constant User::CircleOwner
Which brings us back to the beginning.
How can I do what seems to be a rather common polymorphic :has_many, :through on a module?
Alternatively, is there a good way to allow an object to be collected by arbitrary objects in a similar way that will work with a module?
It turns out that adding :class_name to both :has_many definitions will actually work (someone commented on that, but they deleted their comment). It didn't work in my non-simplified program because of something else in my program that was causing a cascading error that SEEMED to be local to the :has_many definition.
Short story: It was a lot of trouble for something that wasn't actually a problem. Blech
Let's say I have:
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
#...
end
now I want to find all comments on Jim's photo:
#jims_photo = Photo.where(:of => "Jim")
#photo_comments = Comment.where(:commentable => #jims_photo)
this seems to not work in rails (Rails 3). The generated query seems to not expand the polymorphic object into commentable_id and commentable_type fields:
SQLite3::SQLException: no such column: comments.commentable:
I'm new to ruby and rails so I might be using the paradigm incorrectly but my expectation was that rails automatically expands
:commentable => #jims_photo
to:
:commentable_id => #jims_photo.id, :commentable_type => #jims_photo.class.name
If you want to be really safe with:
:commentable_id => #jims_photo.id, :commentable_type => #jims_photo.class.name
Then I'd recommend replacing .class.name with .base_class (you don't actually need the name: to_s returns name and will be called automatically).
The reason for this is that when ActiveRecord saves the _type for a polymorphic association it'll use base_class to ensure it's not saving a class which itself is a polymorphic one.
If you play with store_full_sti_class you'll probably have to take even more precautions.
I highly recommend looking at Rails' STI code, here.
The guides for Rails are one of the best so I'd suggest you start reading about Polymorphic Associations
You class declarations looks OK and I'm assuming that you're migrations is as well. But just for the sake of it. Let's say it looks like this:
class CreateComment < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :name
t.references :commentable, :polymorphic => true
# this is the equivalent of
# t.integer :commentable_id
# t.string :commentable_type
t.timestamps
end
end
end
Not if you have a Article or Photo object and you want to get the comments for that object then Thilo's suggestion is right on. All you need to do is this: #jims_photo.comments
If, on the other hand, you have a an instance of the Comment model, you can get the parent like this: #comment.commentable. But if you want to get Jim's photo comments best to do it like that. Otherwise, you'd have to supply as arguments both the :commentable_id and commentable_type. I'm not aware of any finder that expands the polymorphic object into commentable_id and commentable_type fields for you.
But you can always create a class method for that:
def self.find_by_parent(parent)
where(:commentable_id => parent.id, :commentable_type => parent.class.name)
end
I'm trying to build a database of urls(links). I have a Category model that has and belongs to many Links.
Here's the migration I ran:
class CreateLinksCategories < ActiveRecord::Migration
def self.up
create_table :links_categories, :id => false do |t|
t.references :link
t.references :category
end
end
def self.down
drop_table :links_categories
end
end
Here's the Link model:
class Link < ActiveRecord::Base
validates :path, :presence => true, :format => { :with => /^(#{URI::regexp(%w(http
https))})$|^$/ }
validates :name, :presence => true
has_one :category
end
Here's the category model:
class Category < ActiveRecord::Base
has_and_belongs_to_many :links
end
And here's the error the console kicked back when I tried to associate the first link with the first category:
>>link = Link.first
=> #<Link id: 1, path: "http://www.yahoo.com", created_at: "2011-01-10...
>>category = Category.first
=> #<category id : 1, name: "News Site", created_at: "2011-01-11...
>>link.category << category
=> ActiveRecord::StatementInvalid: SQLite3::Exception: no such column :
categories.link_id:
Are my associations wrong or am I missing something in the database? I expected it to find the links_categories table. Any help is appreciated.
Why are you using HABTM here at all? Just put a belongs_to :category on Link, and a has_many :links on Category. Then in the db, you don't need a join table at all, just a :category_id on the links table.
But, if you do want a HABTM here, from a quick glance, the first thing I noticed is that your join table is named incorrectly -- it should be alphabetical, categories_links.
The second thing is that you can't mix has_one and has_and_belongs_to_many. HABTM means just that -- A has many of B and A belongs to many of B; this relationship implies that the opposite must also be true -- B has many of A and B belongs to many of A. If links HABTM cateogries, then categories must HABTM links.
See http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many