Create join table with no primary key - ruby-on-rails

I have two tables with a many to many relationship that I am using has_and_belongs_to_many to define the association.
class Foo < ActiveRecord::Base
...
has_and_belongs_to_many :bar
...
end
class Bar < ActiveRecord::Base
...
has_and_belongs_to_many :foo
...
end
I also have the class defined to represent the join table
class BarFoo < ActiveRecord::Base
...
belongs_to :foo
belongs_to :bar
...
end
When I run rake db:seed I get the following error:
Primary key is not allowed in a has_and_belongs_to_many join table (bar_foo)
If I edit the database and remove the primary key field (ID) from the bar_foo table and then rerun rake db:seed everything works as desired.
Given the above, what is the preferred means of creating join tables in rails with no primary key?
I also tried using "has_many :bars, :through => :foo" and vise versa but got an error message something like "undefined method 'klass' for nil:NilClass".

Yes, primary key is not allowed for has_and_belongs_to_many.
You have 2 ways to solve this:
Remove the primary key on that table. In your migration class:
create_table :bar_foo, :id => false do |t|
t.integer :bar_id
t.integer :foo_id
end
Apart from this, you will have to delete the file bar_foo.rb from app/models and also remove any fixture and test files that might have been generated. A good idea is to call the script/destroy (or rails destroy) to destroy the files and then regenerating the migration.
Or convert to has_many :through
class Foo < ActiveRecord::Base
...
has_many :bar_foos
has_many :bars, :through => :bar_foos
...
end
class Bar < ActiveRecord::Base
...
has_many :bar_foos
has_many :foos, :through => :bar_foos
...
end
class BarFoo < ActiveRecord::Base
...
belongs_to :foo
belongs_to :bar
...
end

You don't need the model
class BarFoo < ActiveRecord::Base
...
belongs_to :foo
belongs_to :bar
...
end
the has_and_belongs_to_many association will search for a table called bar_foo in your database what you need to do is generate a migration to create this table.
rails generate migration add_table_bar_foo_for_association
then you edit your migration and it should look like this
class AddTableBarFooForAssociation < ActiveRecord::Migration
def up
create_table :bar_foo, :id => false do |t|
t.references :bar
t.references :foo
end
end
def down
drop_table :bar_foo
end
end
Now your association should work and also if you need the association to have extra attributes on the join you can use the has_many :through way and create a model associated to this.

If you want to use a HABTM association you shouldn't create a model for it - just a table bars_foos with bar_id and foo_id integer columns.
If you need the model in between (e.g. if you want to keep track of created_at or some other attributes of the relation) you can add additional model e.g. Barred and then you'd have:
class Foo < ActiveRecord::Base
...
has_many :bars, :through => :barred
...
end
class Bar < ActiveRecord::Base
...
has_many :foos, :through => :barred
...
end
class Barred < ActiveRecord::Base
has_many :bars
has_many :foos
end

Related

how to create a record for a join table?

I have the following associations set up:
class Book < ActiveRecord::Base
belongs_to :author
belongs_to :category
has_many :users_books
has_many :users, through: :user_books
end
and
class User < ActiveRecord::Base
has_many :users_books
has_many :books, through: :users_books
end
I created a join table migration as I ought to
class CreateUsersBooks < ActiveRecord::Migration[4.2]
def change
create_table :users_books do |t|
t.integer :user_id
t.integer :book_id
end
end
end
Now I need to create a method called check_out_book, that takes in a book and a due_date as arguments. When a user checks out a book, it should create a new UserBook record (or Checkout record or whatever you want to call you join table/model). That new UserBook record should have a attribute (and therefore table column) of returned? which should default to false. How would I go about creating this method and the migrations?
Your tablenames and your associations in Rails should always be singular_plural with the exception of the odd duckling "headless" join tables used by the (pretty useless) has_and_belongs_to_many association.
class CreateUserBooks < ActiveRecord::Migration[4.2]
def change
create_table :user_books do |t|
t.references :user
t.references :book
t.boolean :returned, default: false
end
end
end
class UserBook < ActiveRecord::Base
belongs_to :user
belongs_to :book
end
class Book < ActiveRecord::Base
belongs_to :author
belongs_to :category
has_many :user_books
has_many :users, through: :user_books
end
class User < ActiveRecord::Base
has_many :user_books
has_many :books, through: :user_books
end
But you should really use a better more descriptive name that tells other programmers what this represents in the domain and not just a amalgamation of the two models it joins such as Loan or Checkout.
I would also use t.datetime :returned_at to create a datetime column that can record when the book is actually returned instead of just a boolean.
If you want to create a join record with any additional data except the foreign keys you need to create it explicitly instead of implicitly (such as by user.books.create()).
#book_user = Book.find(params[:id]).book_users.create(user: user, returned: true)
# or
#book_user = current_user.book_users.create(book: user, returned: true)
# or
#book_user = BookUser.new(user: current_user, book: book, returned: true)

Naming convention for Rails models based on existing tables

I have two models. Users and Jobs. I have a join table to indicate a User's interest in a Job. I was using a has_and_belongs_to_many relationship for Users and Jobs. I now realize I need to be using a has_many :through relationship. Because of that, I need to have a model for my join table or I get undefined Constant errors. My join table is called jobs_users. What should I name the .rb file for the model and what should the class name be in order to get Rails magic to work? I have found SO questions that imply the class should be JobUser, but nothing on what the .rb file should be named.
Naming convention for Rails models based on existing tables
Model class names use CamelCase. These are singular, and will map automatically to the plural database table name.
> 'JobsUser'.underscore
=> "jobs_user"
This means your file should be named jobs_user.rb. And class will look like below
class JobsUser < ApplicationRecord
...........
end
For fresh migration, you can rollback and destroy current table migration e.g jobs_users and then run this command
rails g model UserJob user:references job:references
db/migrate/TIMESTAMP_create_user_jobs.rb file looks like below
class CreateUserJobs < ActiveRecord::Migration[5.1]
def change
create_table :user_jobs do |t|
t.references :user, foreign_key: true
t.references :job, foreign_key: true
t.timestamps
end
end
end
then migrate this and created model file name is user_job.rb
has_many :through relationship will looks like this
#user.rb
class User < ApplicationRecord
has_many :user_jobs, dependent: :destroy
has_many :jobs, through: :user_jobs
end
#job.rb
class Job < ApplicationRecord
has_many :user_jobs, dependent: :destroy
has_many :users, through: :user_jobs
end
#user_job.rb
class UserJob < ApplicationRecord
belongs_to :user
belongs_to :job
end
That's it

Possible to alias a belongs_to association in Rails?

I have a model with a belongs_to association:
class Car < ActiveRecord::Base
belongs_to :vendor
end
So I can call car.vendor. But I also want to call car.company! So, I have the following:
class Car < ActiveRecord::Base
belongs_to :vendor
def company
vendor
end
end
but that doesn't solve the assignment situation car.company = 'ford', so I need to create another method for that. Is there a simple alias mechanism I can use for associations? Can I just use alias_method :company, :vendor and alias_method :company=, :vendor=?
No it doesn't look for company_id for instance change your code as follows
In Rails3
class Car < ActiveRecord::Base
belongs_to :vendor
belongs_to :company, :class_name => :Vendor,:foreign_key => "vendor_id"
end
In Rails4
We can use alias attribute.
alias_attribute :company, :vendor
In Rails 4, you should simply be able to add alias_attribute :company, :vendor to your model.
Short Version:
Generate model with migration
$ rails generate model Car vendor:references name:string ...
Add following line in Car model i.e car.rb file
class Car < ActiveRecord::Base
belongs_to :company, :class_name => 'Vendor', :foreign_key => 'vendor_id'
end
Now you have #car.company instance method.
For a Detailed explanation read ahead [Optional if you understood the above !!]
Detailed Version:
The model Car will have an association with the model Vendor (which is obvious). So there should be a vendor_id in the table cars.
In order to make sure that the field vendor_id is present in the cars table run the following on the command line. This will generate the right migration. The vendor:references is important. You can have any number of attributes after that.
$ rails generate model Car vendor:references name:string
Or else in the existing migration for create_table :cars just add the line t.references :vendor
class CreateCars < ActiveRecord::Migration
def change
create_table :cars do |t|
t.string :name
...
t.references :vendor
t.timestamps
end
end
end
The final thing that you need to do is edit the model Car. So add this code to your car.rb file
class Car < ActiveRecord::Base
belongs_to :company, :class_name => 'Vendor', :foreign_key => 'vendor_id'
end
After you do the third step you will get the following instance methods for the model Car provided by Rails Associations
#car.company
When you do #car.company it will return a #<Vendor ...> object. To find that #<Vendor ...> object it will go look for the vendor_id column in the cars table because you have mentioned :foreign_key => 'vendor_id'
You can set the company for a car instance by writing
#car.company = #vendor || Vendor.find(params[:id]) #whichever Vendor object you want
#car.save
This will save the id of that Vendor object in the vendor_id field of the cars table.
Thank You.
class Car < ActiveRecord::Base
belongs_to :vendor
belongs_to :company, :class_name => :Vendor
end

Has many :through association not found

I have two models that can have tags added to them.
Player
Ticket
and I have a Tag model which belongs to both so I have two join models
tag_ticket
tag_player
I am getting a Could not find the association :tag_tickets in model Ticket error but my association is in there.
class Ticket < ActiveRecord::Base
has_many :tag_tickets
has_many :tags, :through => :tag_tickets
end
I'm just focusing on the Ticket model but the player model should look similar.
this is my migration for TagTicket
class CreateTagTickets < ActiveRecord::Migration
def change
create_table :tag_tickets do |t|
t.integer :ticket_id
t.integer :tag_id
t.timestamps
end
end
end
You need to specify the :tag_tickets join first like this:
class Ticket < ActiveRecord::Base
has_many :tag_tickets
has_many :tags, :through => :tag_tickets
end
You would also need to specify the joins in your TagTicket model:
class TagTicket < ActiveRecored::Base
belongs_to :ticket
belongs_to :tag
end
Alternatively, you can skip all this and use a habtm join (only recommended if the tag_tickets join is truly only used as a join and has no primary key for itself). In this case you would have no TagTicket model (just a tag_tickets table) and the Ticket model would look like this:
class Ticket < ActiveRecord::Base
has_and_belongs_to_many :tags
end

ROR Associations group by query

I have the following 3 models and relationship between them:
class Calendar < ActiveRecord::Base
has_many:fiscalcalendars
has_many:voucherdatas ,:through => :fiscalcalendars
end
class Fiscalcalendar < ActiveRecord::Base
belongs_to :calendar
has_many :voucherdatas
end
class Voucherdata < ActiveRecord::Base
has_many :fiscalcalendars
has_many :calendars, :through => :fiscalcalendars
end
fields in calendar : id,monthname
fields in fiscalcalendar :id, calendar_id,fiscalyearweek
fields in voucherdata :id, vhour, fiscalyear week
I want the sum of the vhour for each month
I can get it to group by fiscal week by doing
Voucherdata.sum(:vhour,:group=>:fiscalyearweek)
is there a simpler way to get it by month?
This should do what you're asking for, assuming I understand your database relationship.
Voucherdata.sum(:vhour, :joins => :calendars, :group=> 'calendars.monthname)
However this statement won't work without a little modification. You're not telling Rails how to link Voucherdata and Fiscalcalendar. With two :has_many relationships Rails doesn't know where to find the foreign key to link to the other one.
You need to make a join model and either make it a :has_many, :through relationship or use a :has_and_belongs_to_many relationship. Once you've set that up the above statement will work without modification.
Corrected model relationship and migration required. Using a :has_and_belongs_to_many relationship (cleaner syntax):
class Calendar < ActiveRecord::Base
has_many:fiscalcalendars
has_many:voucherdatas ,:through => :fiscalcalendars
end
class Fiscalcalendar < ActiveRecord::Base
belongs_to :calendar
has_and_belongs_to_many :voucherdatas
end
class Voucherdata < ActiveRecord::Base
has_many :calendars, :through => :fiscalcalendars
has_and_belongs_to_many :fiscalcalendars
end
class CreateFiscalcalendarsVoucherdatasJoinTable ActiveRecord::Migration
def self.up
create_table :fiscalcalendars_voucherdatas :id => false do |t|
t.integer :fiscalcalendar_id
t.integer :voucherdata_id
end
end
def self.down
drop_table :fiscalcalendars_voucherdatas
end
end

Resources