Rails inherits subclass - ruby-on-rails

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.

Related

DelegatedType Inheritance

I have two nearly identical models, except one has three or four extra attributes.
I didn't want to use Single Table Inheritance (STI) because instances of model A will have a bunch of nil values that only pertain to model B.
I didn't want to use abstract classes because I can't query across both, and the shared fields are duplicated in each table.
So I thought maybe Delegated Types introduced in rails 6.1 would be a good fit for this. So here is my contrived example:
Automobile holds all the shared fields, LuxuryAutomobile has some extra attributes that only pertain to it.
Migrations:
create_table "automobiles" do |t|
t.integer "user_id"
t.integer "num_wheels"
t.integer "num_doors"
t.string "color"
t.string "autoable_type"
t.integer "autoable_id"
t.references :autoable, polymorphic: true
end
create_table "luxury_automobiles" do |t|
t.boolean "leather_seats"
t.integer "num_screens"
t.string "custom_monogram"
end
create_table "users" do |t|
t.string "name"
end
automobile.rb
class Automobile < ApplicationRecord
belongs_to :user
delegated_type :autoable, types: %w[ Automobile LuxuryAutomobile ]
include Autoable
end
luxury_automobile.rb
class LuxuryAutomobile < ApplicationRecord
include Autoable
delegate :num_wheels, to: :automobile
end
autoable.rb
module Autoable
extend ActiveSupport::Concern
included do
has_one :automobile, as: :autoable, touch: true
end
end
So it's pretty close! I can create Automobiles and LuxuryAutomobiles
u = User.first
Automobile.create! autoable: LuxuryAutomobile.new(custom_monogram: "BMW"), user: u, num_wheels: 6
So I can then call:
la = LuxuryAutomobile.last
la.automobile.num_wheels #returns 6
I can shorten that call further by using delegate in luxury_automobile.rb
delegate :num_wheels, to: :automobile
and in the console:
la = LuxuryAutomobile.last
la.num_wheels #returns: 6
la.custom_monogram #returns: "BMW"
Is there a more seamless way to do this? I feel like it's 90% there, but I can't assign attributes like num_wheels to a LuxuryAutomobile. It would be great if missing attributes from LuxuryAutomobile looked up the chain acted as one with Automobile.
la = LuxuryAutomobile.last
la.num_wheels #returns: 6
la.custom_monogram #returns: "BMW"
la.num_wheels = 8 #NoMethodError (undefined method `num_wheels=' for #<LuxuryAutomobile:0x00007fa1e70fe750>)

Rails 5 - Creating two new Models Inheriting from a base Model

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"

How to set foreign key 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

Relation one to many with 2 foreigns keys of the same table Ruby on rails

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

Custom foreign_key in model gives PG::Error column does not exist - Rails

I have a VideoCollection model that will contain many records from another model (called VideoWork), using the has_many relationship. The VideoCollection model inherits from the Collection model using single table inheritance, while the VideoWork model inherits from the Work model.
I'm having a problem when I try to call up the video_works that belong to a video_collection.
In my video_collection#show action, I use the following to try to display a collection's works:
def show
#video_collection = VideoCollection.find(params[:id])
#collections = #video_collection.children
#works = #video_collection.video_works
end
But when I try to use #works in the show view, I get the following:
PG::Error: ERROR: column works.video_collection_id does not exist
SELECT "works".* FROM "works" WHERE "works"."type" IN ('VideoWork') AND "works"."video_collection_id" = $1
##(Error occurs in the line that contains <% #works.each do |work| %>)
My model files:
#----app/models/video_collection.rb----
class VideoCollection < Collection
has_many :video_works
end
#----app/models/video_work.rb----
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
The "parent" models:
#----app/models/collection.rb - (VideoCollection inherits from this)
class Collection < ActiveRecord::Base
end
#----app/models/work.rb - (VideoWork inherits from this)
class Work < ActiveRecord::Base
end
The Schema file:
#----db/schema.rb----
create_table "works", force: true do |t|
t.string "header"
t.string "description"
t.string "type"
t.string "folder_id"
end
create_table "collections", force: true do |t|
t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
t.text "ancestry"
t.string "name"
t.string "tile_image_link"
end
My Question
I assume that since I have a folder_id column in the works table that I should be able to set up the belongs_to relationship properly, but it seems that Rails still wants me to have a video_collection_id column instead. I would prefer not use something specific like video_collection_id as a foreign key in the works table since I need to set up other relationships (e.g.: photo_collection has_many photo_works, etc).
What am I doing wrong here?
I don't really use has_many and belongs_to with different foreign keys than the standard, but according to the docs I would do this:
class VideoCollection < Collection
has_many :video_works, foreign_key: "folder_id"
end
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
Your Pg error says that the association is looking for 'video_collection_id' instead of 'folder_id'
Guides (chapter 4.3.2.5)

Resources