What is the use of join table in Rails? - ruby-on-rails

For what purposes I'd potentially need to use join table?
For example, when I run rails g migration CreateJoinTable products suppliers, it creates the respective products_suppliers table with products_id and suppliers_id columns. Moreover, these fields have the option :null set to false by default. Why are those fields needed? What they are used for? And what is that null option?
Thanks in advance.

It's standard relational database functionality.
--
Rails is designed on top of a relational database (typically MYSQL or PGSQL), which basically means that you're able to reference "associated" data through the use of foreign keys:
In context of relational databases, a foreign key is a field (or collection of fields) in one table that uniquely identifies a row of another table
In the case of Rails, "relationships" in the database are maintained by ActiveRecord - an ORM (Object Relational Mapper). This means that from the application layer, you just have to focus on populating objects:
#user = User.find x
#user.products #-> outputs records from "products" table with foreign key "user_id = x"
ActiveRecord manages your associations, which is why you have to define the belongs_to / has_many directives in your models etc.
Most associations you create will be reference other tables directly:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :products
end
#app/models/product.rb
class Product < ActiveRecord::Base
belongs_to :user
end
The problem with this is that it only allows you to associate single records; if you wanted to associate multiple (many-to-many), you need a join table.
Rails uses join tables for has_many :through and has_and_belongs_to_many relationships...
Join tables are populated with (at least) the primary key & foreign key of a relationship. For example...
user_id | product_id
1 | 3
1 | 5
2 | 3
This allows you to call:
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :products
end
#app/models/product.rb
class Product < ActiveRecord::Base
has_and_belongs_to_many :users
end
#user.products #-> all products from join table
#product.users #-> all users from join table
In short, if you want to have has_many <-> has_many associations, the join table is necessary to store all the references to the relative foreign keys.

Related

setting up a belongs_to relation when the foreign key is stored in metadata

In a Rails 4 application, I have an STI model that stores metadata in a jsonb column.
Base Class:
class Post < ActiveRecord::Base
...
end
Subclass:
class JobPost < Post
# has a jsonb column for metadata
end
One of the data attributes in the metadata column of a JobPost is a foreign_key reference to another table (company_id). I'd like to add a belongs_to :company reference in the JobPost model. It seems like this should be possible by doing something like
class JobPost < Post
belongs_to :company do
Company.find_by_id self.metadata['company_id']
end
end
but that doesn't appear to work. Help?
Note: I am not necessarily intent on using belongs_to rather than writing needed methods like def company by hand, but I do need a way to eager load companies when listing job posts. If there's a way to do that eager loading without a belongs_to relation I'm all ears.
Update1
I have also tried the following, which doesn't appear to work either:
class JobPost < Post
belongs_to :company, foreign_key: "(posts.metadata->>'company_id')::integer".to_sym
end
Update2
To be more clear about my intentions and need:
1) A JobPost belongs_to a Company, but a Post (and other subclasses of Post) does not. I'd prefer not to jankily add the company_id column to the posts table when it won't be used by the other subclasses.
2) A JobPost could justify having it's own table (perhaps the relationship with a Company is enough to justify it). There are reasons why this wouldn't be ideal, but if that's the only answer I'm open to it. I'd, however, like a more definitive "what you're trying to do can't be done" response before going down this road, though.
The primary question is whether you can customize belongs_to so that it uses the metadata column rather than expecting the foreign key to be a column in the table.
The secondary question is whether you can eager load companies alongside job posts without having that belongs_to relation set up.
EDIT
UPD 2
You need to add "company_id" column to the base class of your STI table. If JobPost inherits from Post, and it should have "company_id" then add the "company_id" column to Post (base table).
Remember STI stands for "Single Table Inheritance" so there is only one table on database schema level. Imagine a column of a Post table, where few data records are the foreign key entries for Companies with company_id and what about the other records of this column with non JobPost subclass types, are they null/empty? Hence the relationship is defined with parent STI table and subclass inherits these relations. Additional type column in STI defines the subclass type.
Check here
You may need to dig further on Polymorphic classes instead of STI if both JobPost and Post have relationship with Company, else create two separate model, as they tend do have some unique relationships and column fields.
UPD
Based on updated ask
app/model/company.rb
class Company < ActiveRecord::Base
has_many :posts
delegate :jobposts, to: :posts
end
app/model/post.rb
class Post < ActiveRecord::Base
belongs_to :company
self.inheritance_column = :ptype
scope :job_posts, -> { where(ptype: 'JobPost') }
def self.ptype
%w(JobPost)
end
end
app/models/jobpost.rb
class JobPost < Post; end
Create a company
company = Company.create!(company_params)
Create some posts and add them to the company
company.posts << JobPost.new(jobpost_params)
To fetch jobpost by company relationship
company.job_posts
In case you are storing company_id in jsonb in any which column, just format your jobpost_params hash input accordingly and it should do the deed for you
OLD ASK
To find by primary key
Company.find(id)
In your case, id is self.metadata['company_id']
To find by other keys
Company.find_by(key: value)
Company.find_by_id is no more recommended
Please remove do and end after belongs_to in your model, instead in your controller you can write:
Jobpost.all.each do |x|
# your do
end
regarding foreign key, as rails is convention over configuration, it by default includes company_id reference to Jobpost which you can change in your Company.rb model

Getting errors with Minitest when I have a timestamp for a has many through jointable

I have a join table for a has many and belongs to many through, the join table including many other attributes has a timestamp, implementation wise there is no trouble,
User
class User < ApplicationRecord
has_many :affiliations
has_many :organizations, through: :affiliations
end
Organization
class Organization < ApplicationRecord
has_many :affiliations
has_many :users, through: :affiliations
end
Affiliation
class Affiliation < ApplicationRecord
belongs_to :user
belongs_to :organization
has_many :xxxxxs
end
Affiliation stores not just the belongs, it itself holds information like what the user's rank and what not is in the organization. It is pretty much a strong model of its own.
For fixtures, I do not have a file for the jointable yet,
user.yml
user1:
email: aaa#aaa.com
organizations: org1
organization.yml
org1
name: foo
but when I run tests using minitest, it gives me an error.
Error:
PublicControllerTest#test_should_get_index:
ActiveRecord::StatementInvalid: Mysql2::Error: Field 'created_at' doesn't have a default value: INSERT INTO `affiliations` (`user_id`, `dominion_id`) VALUES (794918725, 299359653)
Odd thing is, it occurs on tests that don't even use the said table,
class PublicControllerTest < ActionController::TestCase
test "should get index" do
get :index
assert_response :success
end
end
This action does absolutely nothing, at this point its just plain html
class PublicController < ApplicationController
def index
end
end
does nothing in the controller.
They go away when a remove the timestamps, but recording when the association was created is necessary information. Is there something I need to do in the tests?
I am using Rails edge (5.0.0rc1) is there any chance that this is causing the errors?
Update 3.
Having "organizations: org1" for your user1 in fixtures seed data - seems this is causing the issue, because user can be connected to organization only through your joint table.
I didn't find anything explicit in spec, but something relevant here
Fixtures bypass the normal Active Record object creation process.
After reading them from YAML file, they are inserted into database
directly using insert query. So they skip callbacks and validations
check. This also has an interesting side-effect which can be used for
drying up fixtures.
Update 2.
I was wrong at assumption that you can't have timestamps in has_and_belongs_to_many jointable managed by Rails. In fact, inside HasAndBelongsToMany Rails will create an ActiveRecord::Base class for that table - here
def through_model
habtm = JoinTableResolver.build lhs_model, association_name, options
join_model = Class.new(ActiveRecord::Base) {
class << self;
...
And ActiveRecord::Base include Timestamp module
So your error should be caused by some other way of creating an entry in jointable other then standard Rails association.
Original.
I don't believe that you can have automatically managed timestamp fields in jointable for has_and_belongs_to_many relation in ActiveRecord. This didn't (intentionally) work in old Rails (e.g. 3.2 - link below), and it don't sound like it changed recently.
If you want to have extended join table, you may create a dedicated ActiveRecord model for it and use use has_many :through association. This way it will automatically support timestamps should you add it to table definition.
See https://github.com/rails/rails/issues/4653 for timestamps on HABTM jointable
AFAICT Rails 3.1 does not populate timestamps on a join table. The
only difference is that in 3.2, when you add timestamps, they are
marked as NOT NULL.
#veganstraightedge the timestamps didn't "work" in 3.1 - they just
didn't raise an error when the join table was saved with them as null.
the difference here is that in 3.2 timestamps are created with a NOT
NULL constraint.
Basically, this can come from an idea that you don't have ActiveRecord model class for the jointable (update 2 - actually you have!), and timestamps are feature of ActiveRecord model. Timestamps in Rails 5.0rc1 hasn't changed a lot - sources - Timestamp is a module that extends ActiveRecord class.
By the way, it's now suggested to use create_join_table migration helper that will create "pure" table (two id's only, no timestamps):
https://github.com/rails/rails/pull/4726
SO Question with similiar error - Rails 3.2 + MySQL: Error: Field 'created_at' doesn't have a default value: INSERT INTO
Rails 3.2 doesn't automatically populate the timestamp fields for join
tables of :habtm relationships.
Alternatively (warning - theory!), you can try using either Association callbacks or Association extensions - http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Association callbacks
Similar to the normal callbacks that hook into the life cycle of an
Active Record object, you can also define callbacks that get triggered
when you add an object to or remove an object from an association
collection.
class Project
has_and_belongs_to_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
...
end
end
Extensions
The extension argument allows you to pass a block into a
has_and_belongs_to_many association. This is useful for adding new finders, > creators and other factory-type methods to be used as part of
the association.
has_and_belongs_to_many :contractors do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
Extensions can refer to the internals of the association proxy using
these three attributes of the proxy_association accessor:
proxy_association.owner returns the object that the association is a part of.
proxy_association.reflection returns the reflection object that describes the association.
proxy_association.target returns the associated object for belongs_to or has_one, or the collection of associated objects for has_many or has_and_belongs_to_many.
Your relationships between models cannot be represented by a join table. They need to be represented by a regular, model table (and it needs id, created_at, updated_at). They should all be regular, ActiveRecord::Base models.
In this case, you have a join model, Affiliation, and not a join table
So for all three, you need a regular active record base table (containing id, created_at, updated_at)
Join tables are only used for has_and_belongs_to_many associations, and these do allow tables with no id, created_at, updated_at

one to one or zero association in rails

Model I
class TimeLog < ActiveRecord::Base
has_one :custom_time_fields, :dependent => :destroy
end
Model II
class CustomTimeFields < ActiveRecord::Base
belongs_to :time_log
end
above design in terms of database will be
timelogs table + custom_time_field_id(foreign key)
custom_time_fields
So when i delete timelog entry its associated 'custom_time_field' will be auto deleted by rails
But i want database design like following
Table I:
time_logs
Table II
custom_time_fields (having time_log_id as foreign key)
Table I will have Zero or one association of Table II
How can i represent above database design in Rails models, so that when i delete time_log, associated custom_time_field entry is auto deleted.
You have to switch the has_one and belongs_to relations of your models to change the table containing the foreign key (the model with the relation belongs_to is the one holding the foreign key). Do not forget to adapt your migrations according to the change (to declare the time_log_id column).
I think the "zero or one" relation you're looking for is the has_one relation. This relation is not mandatory (unless you add a validation to it).

has_and_belongs_to_many where both models have underscore names

I'm working with Rails 3.2.1 and have the two models CookingVenue and DiningVenue with associated MySQL tables of cooking_venues and dining_venues. I have set up the has and belongs to many relationship between the two models but what's the name of the MySQL table name here to represent the join?
Is it cooking_venues_dining_venues?
Will Rails try to find habtm relationships between cooking and venues etc, or is Rails really clever enough to work all this out?
Like you said, cooking_venues_dining_venues is the name of the join table. After creating this table with cooking_venue_id and dining_venue_id field you need to define has_and_belongs_to_many association in both model.
class CookingVenue < ActiveRecord::Base
has_and_belongs_to_many :dining_venues # foreign keys in the join table
end
class DiningVenue < ActiveRecord::Base
has_and_belongs_to_many :cooking_venues # foreign keys in the join table
end
Yup. You just name the join table in alpha order like you have done and you should be good to go.

Rails relationship

I'm trying to figure out something regarding rails relationships. I already posted a question regarding a specific items not long ago but I do not really understand what's done in the underlying DB.
I have a Project model and a Client model.
A Project belongs_to :client => I need to manually add client_id in projects table (with a migration).
A Client has_many :projects => I do not need to do anything in the DB (no migration).
The project.client and client.projects methods are both available.
I have a Group model and a User model.
A Group has_and_belongs_to_many :user
A User has_and_belongs_to_many :group
I then need to create a migration to create a joint table with a user_id and a group_id pointers.
I do not really see where the border between rails and the relational database is.
Why do I need to add foreign key sometimes but not always ? How is the has_many relationship handled as I did not do anything in the underlying DB for this particuliar guy ?
I am kind of lost sometimes :)
Thanks and Regards,
Luc
For a has_many <-> belongs_to assoication, you're defining that one project is owned (belongs_to) by one client. Therefore, that client has many (has_many) projects. For a project to determine what client it belongs to it needs to have an client_id column so that it can look it up. This client_id column is used by Rails when you call the client method, much like this:
Client.find(project.client_id)
That's how you can find a project's client. The client_id column is often referred to as a foreign key, because its a unique identifier ("key") in a table not of its origin ("foreign"). Boom.
When you call the other way around, finding all the projects a client has, i.e. client.projects, Rails does the equivalent of this:
Project.find_all_by_client_id(client.id)
This then returns all Project records which are associated with a particular client, based off the client_id field in the projects table.
With a has_and_belongs_to_many association, such as your users & groups example, you're declaring that a user has_and_belongs_to_many :groups.
Now if it were simply a has_many :groups, the foreign key would go in the groups table, or if it were a belongs_to it would go in the users table. Good thing to remember: the foreign key always goes in the table of the model that has the belongs_to.
You're also declaring that a group has_and_belongs_to_many :users, and so we come across the same problem. We can't declare the key in the users table because it doesn't belong there (because a user has many groups, you would need to store all the group ids the user belongs to) or the groups table for the same reasons.
This is why for a has_and_belongs_to_many we need to create what's known as a join table. This table has two and only two fields (both of them foreign keys), one for one side of the association and another for the other. To create this table, we would put this in a migration's self.up method:
create_table :groups_users, :id => false do |t|
t.integer :group_id
t.integer :user_id
end
A couple of things to note here:
The table name is the two names of the two associations in alphabetical order. G comes before U and so the table name is groups_users.
There's the :id option here which, when given the value of false generates a table with no primary key. A join table doesn't need a primary key because its purpose is to just join other tables together.
We store the group_id and user_id as integer fields, just like we would on a belongs_to association.
This table will then keep track of what groups have what users and vice versa.
There's no need to define additional columns on either the users or groups table because the join table has got that under control.
class Customer < ActiveRecord::Base
has_many :orders, dependent: :destroy
end
class Order < ActiveRecord::Base
belongs_to :customer
end
#order = #customer.orders.create(order_date: Time.now)

Resources