Lets say that I have an User model with its attributes (first_name, last_name, etc) and I want to create two new models Teacher and Student.
They will inherit the User model attributes, and also, they will have specific attributes. For instance, the Student model will have a file attribute, and the Teacher model will have subject attribute.
I was reading about STI (Single Table Inheritance) and Polymorphic relationships.
What should I look for to accomplish this? Do you have any example to show?
If you create an attribute called "type" on your users table, Rails will automatically assume you want to implement STI. Then, creating Teacher and Student models is as simple as extending the User class. The name of the child class will automatically be inserted into the type column and used to filter queries as you would expect.
user.rb
class User < ApplicationRecord
end
teacher.rb
class Teacher < User
end
student.rb
class Student < User
end
With STI, you place all of the columns that either model will use in the same table and simply ignore (default to null) the ones that don't apply in any give situation.
A polymorphic relationship allows two or more tables to fill the same association. If you want to use three different tables but ensure that a User has either a Teacher or a Student, that could be modeled as a polymorphic belongs_to. The downside is that you would need to get back to the User model to access the shared information, i.e. teacher.user.first_name.
I have found this gem that looks like what I am looking for. I have played with it a little and it does work for me.
https://github.com/krautcomputing/active_record-acts_as
So, for my case I have added to the Gemfile:
gem 'active_record-acts_as'
Then:
$ bundle
These are my migrations:
# 20171202142824_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.date :birth_date
t.string :dni
t.string :cuil
t.string :email
t.string :phone
t.string :address
t.string :postal_code
t.string :city
t.string :state
t.string :country
t.actable # This is important!
t.timestamps
end
end
end
# 20171202142833_create_students.rb
class CreateStudents < ActiveRecord::Migration[5.1]
def change
create_table :students do |t|
t.string :file
# Look, there is no timestamp.
# The gem ask for it to be removed as it uses the User's timestamp
end
end
end
# 20171202142842_create_teachers.rb
class CreateTeachers < ActiveRecord::Migration[5.1]
def change
create_table :teachers do |t|
# Look, there is no timestamp.
# The gem ask for it to be removed as it uses the User's timestamp
end
end
end
These are my models:
# user.rb
class User < ApplicationRecord
actable
validates_presence_of :first_name, :last_name
def full_name
[last_name.upcase, first_name].join(', ')
end
end
# student.rb
class Student < ApplicationRecord
acts_as :user
validates_presence_of :file
end
# teacher.rb
class Teacher < ApplicationRecord
acts_as :user
end
Now, with all that set, you can simply create a new Student and a new Teacher doing:
Student.create!(first_name: 'John', last_name: 'Doe', file: 'A125')
=> #<Student id: 3, file: "A125">
Teacher.create!(first_name: 'Max', last_name: 'Power')
=> #<Teacher id: 1>
You have access to all the methods and attributes of the User. For instance:
Teacher.last.full_name
=> "POWER, Max"
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 5 years ago.
Improve this question
I have a Person class which I want to represent contributors such as chairs and vice-chairs of committees (and later, for other things). So in my Committee class, I want to use the Person class to provide a chair and many vice-chairs:
Committee has many ViceChairs
Committee has one Chair
Chairs and ViceChairs are both People.
I expected to say
class Committee < ActiveRecord::Base
has_many :vice_chairs, class_name: 'People'
has_one :chair, class_name: 'People'
end
class Person < ActiveRecord::Base
belongs_to :group, foreign_key: 'vice_chair_id'
belongs_to :group, foreign_key: 'chair_id'
end
Is this the right approach?
Update: I have been advised to use Single Table Inheritance to solve this issue. I changed my code like this:
class Person < ActiveRecord::Base
end
class Chair < Person
end
class ViceChair < Person
end
class CreatePeople < ActiveRecord::Migration
def change
create_table :people do |t|
t.string :type
t.string :first_name
t.string :last_name
t.string :email
t.string :affiliation
t.timestamps null: false
end
end
end
Will this work? How do I write the migrations to support this?
A use case where you want slightly different behaviour based on the type of model but where the attributes are the same, it is best to use Single Table Inheritance [1].
So I recommend the following:
Models:
class Group < ApplicationRecord
has_many :vice_chairs
has_one :chair
end
class Person < ApplicationRecord
belongs_to :group
end
class ViceChair < Person
belongs_to :group
end
class Chair < Person
belongs_to :group
end
Migrations:
class CreateGroups < ActiveRecord::Migration[5.0]
def change
create_table :groups do |t|
t.string :name
t.timestamps
end
end
end
class CreatePeople < ActiveRecord::Migration[5.0]
def change
create_table :people do |t|
t.string :type
t.string :full_name
t.integer :age
t.references :group, foreign_key: true
t.timestamps
end
end
end
Example Usage:
# create some test data
grp = Group.create!(name: "superheros")
vc = ViceChair.create!(full_name: "Superman Sam")
c = Chair.create!(full_name: "Batman Bob")
p = Person.create!(full_name: "Regular John Doe")
# add to group's ViceChairs
grp.vice_chairs << vc
# add Chair to group
grp.chair = c
grp.save!
# Convert person to vice chair
p.type = "ViceChair"
p.save!
new_vc = ViceChair.where(full_name: p.full_name).first
grp.vice_chairs << new_vc
grp.vice_chairs.count
# => 2
rails CLI commands:
rails generate model Group name
rails generate model Person type full_name age:integer group:references:index
rails generate model ViceChair group:references --parent=Person
rails generate model Chair group:references --parent=Person
Handling business logic of managing changes to Chair:
You will need to add some custom logic to manage the Chair association without creating dangling chairs. I recommend adding an instance method to the Group class to add_chair and remove_chair then you need to decide what happens when a Person is removed from the chair position. They probably become an ordinary person. In which case you can set the type to nil.
Enjoy!
[1] More info on STI at http://guides.rubyonrails.org/association_basics.html#single-table-inheritance
If you have common fields with different model name, in that case you should use Single Table Inheritance just by adding a type field in the table. This field contains the model name.
You can refer the below link for Single table inheritance http://eewang.github.io/blog/2013/03/12/how-and-when-to-use-single-table-inheritance-in-rails/
I Have a model named employee. The following is my migration file.
class CreateEmployees < ActiveRecord::Migration
def change
create_table :employees, id: false do |t|
t.string :name
t.string :password
t.string :role
t.primary_key :name
end
end
end
Now, I want to create a model named "teamplayer" with the columns as 'name' which needs to refers 'name' column in employee model. And 'tl' column
which is independent to this model. The following is my "teamplayer" migration file.
class CreateTeamplayers < ActiveRecord::Migration
def change
create_table :teamplayers, :id false do |t|
t.string :tl
t.string :name
end
end
end
In the above file, how to reference 'name' column to the model employee? So how to achieve foreign key in rails.
I think you want to look into Active Record Associations (http://guides.rubyonrails.org/association_basics.html)
I know you've asked to create a foreign key on name but unless you plan to ensure that name is unique, then this is possibly not the best plan (depending on the actual relationship you are trying to model - one to many / one to one etc).
I would be tempted to set up the foreign key relationship on employees.id. To do this, you can use the has_many and belongs_to associations.
You could change your teamplayers migration as follows:
class CreateTeamplayers < ActiveRecord::Migration
def change
create_table :teamplayers, :id false do |t|
t.belongs_to :employee
t.string :tl
end
end
end
Then in your Employee model, you can add the has_many side of things:
class Employee < ActiveRecord::Base
has_many :teamplayers
end
You can still easily get the Employee name given a Team Player record with a simple join.
Edit - to get the Employee, you can do something like this, assuming #tis a teamplayer instance:
#t.employee.name
(the code is untested and from memory so....)
You can do it in the teamplayer model, you just need to add index in your migration
class CreateTeamplayers < ActiveRecord::Migration
def change
create_table :teamplayers, :id false do |t|
t.string :tl
t.string :name
end
add_index :teamplayers, :name
end
end
You can set name as Primary key inside the employee model like this
class Employee < ActiveRecord::Base
self.primary_key = "name"
has_many :teamplayers
end
Now inside the model Teamplayer you can set the foreign key
class Teamplayer < ActiveRecord::Base
belongs_to :employee, foreign_key: 'name'
end
This should reference 'name' to employee model
I have this three classes user, driver, company.
every company or driver belongs a user. The models look like
class Company < User
has_many :driver
end
class Driver < User
end
class User < ActiveRecord::Base
enum role: [:admin, :support, :B2B , :B2C]
end
and the database looks like
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
t.timestamps null: false
end
end
end
class CreateCompanies < ActiveRecord::Migration
def change
create_table :companies do |t|
t.string :comp_name
t.string :first_name_counterpart
t.string :last_name_counterpart
t.string :iban_nr
t.string :bic
t.string :email_counterpart
t.string :addresse
t.string :city
t.string :zip
t.references :user
t.timestamps null: false
end
end
end
class CreateDrivers < ActiveRecord::Migration
def change
create_table :drivers do |t|
t.string :first_name
t.string :last_name
t.date :birthday
t.integer :sex
t.integer :dpi
t.integer :score
t.references :user
t.timestamps null: false
end
end
end
Why can't I create a Driver-instance. For example, if I try d = Driver.new, I get a user-instance.d = Driver.new
=> #<Driver id: nil, email: nil, created_at: nil, updated_at: nil>
This is how Rails guesses the table name from the model classes. Quoting from the ActiveRecord docs for table_name:
Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used to guess the table name even when called on Reply.
You should be able to force the proper table name by the table_name= setter, e.g.:
class Driver < User
self.table_name = "drivers"
end
On the other hand, I am also not sure that your approach (with such inheritance) will not cause problems somewhere else.
If you have models with inheritance like you do:
class User < ActiveRecord::Base
enum role: [:admin, :support, :B2B , :B2C]
end
class Company < User
has_many :driver
end
class Driver < User
end
rails infers that you are after Single Table Inheritance (STI) and expects there is just a base table users with a column type which stores the records of User, Company and Driver with actual class name (ex: Company or Driver etc).
If you would rather want to have separate tables users, companies and drivers because each of those tables have different set of columns, and the only reason why you are put inheritance in place is to share some common functionality, then you should extract the common functionality into modules and mix them into those models (by just inheriting from ActiveRecord::Base.
rails, through active_support provides whats called concerns to extract the common functionality into modules and mix them intuitively.
You could probably get away with inheritance and still have these models point to separate tables with the declaration of self.table_name = "table_name". But it is not a good idea, as it goes around the rails conventions and may cause problems down the lane.
Refer to ActiveRecord::Inheritance and ActiveSupport::Concern for more info.
I have a table called customers. These table has two addresses . One address of work and One direccion of house
Those 2 addresses belong to a table called addresses
I don't know how to relation those 2 tables
Migrations
class CreateCustomers < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.integer :address_id #Address of work
t.integer :address_id_1 #Address of home
t.timestamps
end
end
end
class CreateAdresses < ActiveRecord::Migration
def change
create_table :adresses do |t|
t.string :street
t.timestamps
end
end
end
I do not believe this is a good approach or database design. If you want to proceed this way and not get out of the rails convention just create two columns address_id and address_two_id
and in customer.rb
belongs_to :address, class_name: "Address"
belongs_to :address_two, class_name: "Address"
By default rails takes the name of the foreign key and stores it in a column called "name"+"_id"
The better way is two have a column customer_id in your Address model and create a relation in your customer class
customer.rb
has_many :addresses
And you can also validate that a customer has no more than two addresses by adding this validation to
address.rb
validate :validate_two_addresses
def validate_two_addresses
address_count = Address.where(customer_id: self.customer_id).count
errors.add(:base, "You cannot have more than 2 addresses.") unless address_count < 3
end
I created a rails model by doing
script/generate model Customer name:string address:string city:string state:string zip:integer [...]
I filled the database with 5000 customers and started building my app. Now I've realized my model isn't normalized: I often have multiple customers at the same address! If I wish to do something per-address, like, say, a mailing, this causes problems. What I'd like to have is a Address model, a Customer model, and a Mailing model.
Is there a rails way to normalize an existing model, splitting it into two models? Or should I just write a script to normalize my existing data, then generate new models accordingly?
You asked about what the migration would look like. Rather than cram this in a comment reply, I created a new answer for you.
script/generate model address customer_id:integer address:string city:string state:string zip:integer
class CreateAddresses < ActiveRecord::Migration
def self.up
create_table :addresses do |t|
t.integer :customer_id
t.string :address
t.string :city
t.string :state
t.integer :zip_code
t.timestamps
end
# move customer address fields to address table
Customer.all.each do |c|
Address.create({
:customer_id => c.id,
:address => c.address,
:city => c.city,
:state => c.state,
:zip => c.zip
})
end
# you'll have to write your own merge script here
# use execute("your sql goes here...") if you can't do it with ActiveRecord methods
# remove old customer address columns
remove_columns(:customers, :address, :city, :state, :zip)
end
def self.down
# here you will define the reverse of the self.up method
# re-add the address columns to the user table
# repopulate the customer table with data from the address table
drop_table :addresses
end
end
Resources
AcitveRecord::Migration
execute
I'm not aware of a built-in Rails way to programmatically split up your model. You'll need to write a new Address model and database update migration to get everything switched over.
Your models will likely look something like this:
class Person < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
end