Rails: self join scheme with has_and_belongs_to_many? - ruby-on-rails

I would like to create a structure of Users having many friends, also of class User:
class User < ActiveRecord::Base
has_and_belongs_to_many :friends, class_name: "User"
end
I do not need any details of their relationship thus I do not use :through with kind of class Friendship. But now I cannot find any way how to create corresponding database (neither with migration file nor using rails g model User username:string ... command). Any ideas?

Here are some resources which may be helpful:
RailsCasts episode #163 Self-Referential Association regarding self-referential many-to-many relationships
RailsCasts episode #47 Two Many-to-Many. This might be more relevant to what you're attempting to accomplish
A gist someone created for self-referential relationships using HABTM
I'll summarize the information found in those links:
Given that you're describing a self-referential many-to-many relationship, you will of course end up with a join table. Normally, the join table should be deliberately named in such a way that Rails will automatically figure out which models the table is joining, however the "self-referential" part makes this a tad awkward, but not difficult. You'll merely have to specify the name of the join table, as well as the joining columns.
You'll need to create this table using a migration that will probably look something like this:
class CreateFriendships < ActiveRecord::Migration
def self.up
create_table :friendships, id: false do |t|
t.integer :user_id
t.integer :friend_user_id
end
add_index(:friendships, [:user_id, :friend_user_id], :unique => true)
add_index(:friendships, [:friend_user_id, :user_id], :unique => true)
end
def self.down
remove_index(:friendships, [:friend_user_id, :user_id])
remove_index(:friendships, [:user_id, :friend_user_id])
drop_table :friendships
end
end
I'm not certain whether there is a shortcut way of creating this table, but bare minimum you can simply do rails g migration create_friendships, and fill in the self.up and self.down methods.
And then finally in your user model, you simply add the name of the join table, like so:
class User < ActiveRecord::Base
has_and_belongs_to_many :friends,
class_name: "User",
join_table: :friendships,
foreign_key: :user_id,
association_foreign_key: :friend_user_id
end
As you can see, while you do have a join table in the database, there is no related join model.
Please let me know whether this works for you.

Related

How to Manage Many-to-Many Relationships

I have two models Group and Person that I want to have a many-to-many relationship, but I'm unclear on how to manage the relationship itself. I want to be able to create groups and persons separately -- NOT necessarily via a nested model -- and then link persons to groups from the group view/model itself.
Does anyone have any suggestions on how to do so?
I thought of creating a many-to-many relationship via a join model and then accepting nested attributes for the join model in the Group model -- so I believe I will be able to add and remove relationships via the Group view/model. Does this approach make sense?
I would create a PersonGroup model that looks like this:
class PersonGroup < ActiveRecord::Base
has_many :people
has_many :groups
end
And you might also do rails generate migration create_person_group and put this in the up method of the generated migration file:
create_table :person_group do |t|
t.integer :person_id, :null => false
t.integer :group_id, :null => false
t.timestamps
end
add_index :person_group, [:person_id, :group_id], :unique => true
Then in Person:
class Person < ActiveRecord::Base
has_many :person_groups
has_many :groups, :through => :person_groups
end
And in Group:
class Group < ActiveRecord::Base
has_many :person_groups
has_many :people, :through => :person_groups
end
Create a junction table. Junction tables are used when you want to store many-to-many relationships. I don't develop in ROR so I don't know the specifics for ActiveRecord but I am sure that this can help you think about the problem as well.
group_id INTEGER,
person_id INTEGER

Rails Many to Many SQLite3 error

I created a many-to-many relationship in rails, here's my models and migrations
class Channel < ActiveRecord::Base
has_and_belongs_to_many :packages
validates_presence_of :name
end
class Package < ActiveRecord::Base
has_and_belongs_to_many :channels
validates_presence_of :name
end
class CreateChannelsPackages < ActiveRecord::Migration
def change
create_table :channels_packages, :id => false do |t|
t.references :channel
t.references :package
t.timestamps
end
add_index :channels_packages, :channel_id
add_index :channels_packages, :package_id
end
end
Then i have a multiple select, but when i try to save i get this error
SQLite3::ConstraintException: constraint failed: INSERT INTO "channels_packages" ("package_id", "channel_id") VALUES (1, 1)
I tried to remove the indexes from the migration but it didn't solve it, did somebody else have this problem?
Btw i'm using Rails 3.2.6 and sqlite3 1.3.6
I think gabrielhilal's answer is not quite correct: use of extra attributes in the join table is deprecated, thus you need to remove the timestamp in your migration, then it should work just fine with the has_and_belongs_to_many wich itself is not deprecated.
If you do need additional attributes in your join table, though, has_many :through is the way to go.
There is also another question with good answers on this topic:
Rails migration for has_and_belongs_to_many join table
I don't know if it is the reason of your problem, but the has_and_belongs_to_many association is deprecated.
According to the Rails Guide:
The use of extra attributes on the join table in a has_and_belongs_to_many association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a has_many :through association instead of has_and_belongs_to_many.
I know that you are not adding any extra attribute to the join table, but try changing your migration to the below, which I think is the default:
class CreateChannelPackageJoinTable < ActiveRecord::Migration
def change
create_table :channels_packages, :id => false do |t|
t.integer :channel_id
t.integer :package_id
t.timestamps
end
end
end

How to Nest Models within a Model

Imagine I have two models
Film
-name
-description
-duration
-year_made
-rating
-actors
Actor
-name
-d_o_b
-biography
-films
Actors are nested in a Film and vice versa.
How do I represent this relationship in my Ruby models? Realistically I would have a third table mapping actor_id with film_id.
Whilst adding details to a film I would like to be able to create an actor on the fly(if an actor does not exist create a new one with the name supplied)
Thank you in advance.
ADDITION:
Just found a link to a similar question.
You're looking at a Has and Belongs to Many (HABTM) relationship between the two tables.
Read about HABTM relationship in the Rails guides here: http://edgeguides.rubyonrails.org/association_basics.html#has_and_belongs_to_many-association-reference
First you'll need to generate a migration which will look something like this:
class AddActorFilmTable < ActiveRecord::Migration
def self.up
create_table :actors_films, :id => false do |t|
t.integer :actor_id, :null => :false
t.integer :film_id, :null => :false
end
add_index :actors_films, [:actor_id, :film_id], :unique => true
end
def self.down
drop_table :actors_films
end
end
and then specify in your models:
class Actor < ActiveRecord::Base
has_and_belongs_to_many :films
end
class Film < ActiveRecord::Base
has_and_belongs_to_many :actors
end
This will allow you to use all of the additional Rails methods for this type of relationship. To use this in a form, you could follow RailsCast 17: HABTM Checkboxes - though it's old, it should still apply. Alternatively, you can use a gem like Simple Form to easily generate the associations for you like so:
form_for #actor do |f|
f.collection_check_boxes :film_ids, Film.all, :id, :name
end

Rails Model multiple column uniqueness

I am making a Viewer model with
belongs_to :users
belongs_to :orders
that joins the models Users and Orders with a :has_many :through => :viewers.
And the Viewer model has the attributes of user_id and order_id.
How would I set it up so that new viewers are only accepted if both user_id and order_id are unique in the same row? I remember in MySQL being able to do so with a flag (although I can't for the life of me remember what it was), but I'm not sure how to do it with Rails.
Can I do something like (for Viewer.rb) validates_uniqueness_of :user_id, :scope => :order_id?
Oh.
I think the way to do so is as follows:
in the Viewer model migration file (ie: I should have done this earlier)
def self.up
#create_table code
end
add_index :viewers, [:user_id, :order_id], :unique => true
end

Scaffolding ActiveRecord: two columns of the same data type

Another basic Rails question:
I have a database table that needs to contain references to exactly two different records of a specific data type.
Hypothetical example: I'm making a video game database. I have a table for "Companies." I want to have exactly one developer and exactly one publisher for each "Videogame" entry.
I know that if I want to have one company, I can just do something like:
script/generate Videogame company:references
But I need to have both companies. I'd rather not use a join table, as there can only be exactly two of the given data type, and I need them to be distinct.
It seems like the answer should be pretty obvious, but I can't find it anywhere on the Internet.
Just to tidy things up a bit, in your migration you can now also do:
create_table :videogames do |t|
t.belongs_to :developer
t.belongs_to :publisher
end
And since you're calling the keys developer_id and publisher_id, the model should probably be:
belongs_to :developer, :class_name => "Company"
belongs_to :publisher, :class_name => "Company"
It's not a major problem, but I find that as the number of associations with extra arguments get added, the less clear things become, so it's best to stick to the defaults whenever possible.
I have no idea how to do this with script/generate.
The underlying idea is easier to show without using script/generate anyway. You want two fields in your videogames table/model that hold the foreign keys to the companies table/model.
I'll show you what I think the code would look like, but I haven't tested it, so I could be wrong.
Your migration file has:
create_table :videogames do |t|
# all your other fields
t.int :developer_id
t.int :publisher_id
end
Then in your model:
belongs_to :developer, class_name: "Company", foreign_key: "developer_id"
belongs_to :publisher, class_name: "Company", foreign_key: "publisher_id"
You also mention wanting the two companies to be distinct, which you could handle in a validation in the model that checks that developer_id != publisher_id.
If there are any methods or validation you want specific to a certain company type, you could sub class the company model. This employs a technique called single table inheritance. For more information check out this article: http://wiki.rubyonrails.org/rails/pages/singletableinheritance
You would then have:
#db/migrate/###_create_companies
class CreateCompanies < ActiveRecord::Migration
def self.up
create_table :companies do |t|
t.string :type # required so rails know what type of company a record is
t.timestamps
end
end
def self.down
drop_table :companies
end
end
#db/migrate/###_create_videogames
class CreateVideogames < ActiveRecord::Migration
create_table :videogames do |t|
t.belongs_to :developer
t.belongs_to :publisher
end
def self.down
drop_table :videogames
end
end
#app/models/company.rb
class Company < ActiveRecord::Base
has_many :videogames
common validations and methods
end
#app/models/developer.rb
class Developer < Company
developer specific code
end
#app/models/publisher.rb
class Publisher < Company
publisher specific code
end
#app/models/videogame.rb
class Videogame < ActiveRecord::Base
belongs_to :developer, :publisher
end
As a result, you would have Company, Developer and Publisher models to use.
Company.find(:all)
Developer.find(:all)
Publisher.find(:all)

Resources