Same Model with different columns Rails - ruby-on-rails

I have 2 user roles Developer and Driver. They both are under an User model, but also both have different details such as Developer has hourly_rate, skills, experience, full_name and Driver has cars_he_can_drive, hours_driven, full_name etc.
They have some common columns and some different ones as well. Should there be a separate detail table (develop_details, driver_details) for each of the User? and further, relationships can be made with them.
Else I can have same model with all columns and fetch only the ones required (others will be nil ofcourse).
UPDATE
Im using role as and integer in user table and then using enums.
I'm using Rails 5 with devise 4.3.0.

You probably want to look at Single Table Inheritance
Try a Developer and Driver that both inherit from User and share one users database table. Each is effectively its own model, allowing you to define totally independent associations, callbacks, validations, instance methods, etc...
They share all of the same database columns, and anything defined in the User class will be inherited (and can be overwritten).
You will need to add a type column to users. All of the Developer and Driver columns fields should be defined for the users table as well.
class AddTypeColumnToUsers < ActiveRecord::Migration
def change
add_column :users, :type, :string
end
end
And your models
class User < ApplicationRecord
end
class Driver < User
end
class Developer < User
end
Driver.new.type # => "Driver"
Developer.new.type # => "Developer"
User.new.type # => nil
User.new(type: 'Driver').class # => Driver
User.new(type: 'Developer').class # => Developer
User.new.class # => User
You can run separate queries for them just as you would any other model
Developer.all # queries all users WHERE type="Developer"
Driver.all # queries all users WHERE type="Driver"
User.all # queries all users no matter what type
Write your associations just as you would with any other model, and ActiveRecord will take care of everything else.
class Company < ApplicationRecord
has_many :users
has_many :developers
has_many :drivers
end
class User < ApplicationRecord
belongs_to :company
end
class Driver < User
belongs_to :company
end
class Developer < User
belongs_to :company
end
c = Company.first
c.developers # => All developers that belong to the Company
c.drivers # => All drivers that belong to the Company
c.users # => All users (including developers and drivers) that belong to the Company
You can also use enum for the type column, and override the default type column name if you wish
class AddTypeColumnToUsers < ActiveRecord::Migration
def change
add_column :users, :role, :integer
end
end
class User < ApplicationRecord
self.inheritance_column = :role # This overrides the the "type" column name default
enum role: { Driver: 0, Developer: 1 }
end
class Driver < User
end
class Developer < User
end
The catch with using enum is you will have to use the capitalized name, and all of your enum helper methods and scopes will be capitalized as well.
User.Driver # => Scope that returns all drivers
Driver.all # => same as User.Driver
User.first.Driver? # => Is the user a Driver?
http://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html
http://siawyoung.com/coding/ruby/rails/single-table-inheritance-in-rails-4.html

Well both approach will solve your problem. Personally I will only have a single table because Developer is also type of User and Driver is also type of User. You can do something like this:
class User < ActiveRecord::Base
end
class Developer < User
end
class Driver < User
end
If user is Developer then you can fetch extra columns for developers. It's fine to have few column with nil value.

You can decide this based on the number of different columns. If the number of different columns is more which seems to be the case here, Create a user table which contains all the common columns and 2 more tables(developers and drivers) which will contain a user_id to map with the user.
Also create a table named roles. Roles will contain id and role_name(driver, developer etc.) and add role_id column in users table. This structure will give you the flexibility even if you have more than 2 roles.
class User < ActiveRecord::Base
end
class Driver < ActiveRecord::Base
belongs_to :user
end
class Developer < ActiveRecord::Base
belongs_to :user
end
class Role < ActiveRecord::Base
end

Related

ActiveRecord query with multiple nested relations for the same model in where clause

I'm using:
Ruby 2.6.5
Rails 6.0.2.1
PostgreSQL 11
The problem:
I'm trying to create a Scope query for Pundit to check if user has an access to the secret (password) that it wants to visit. I tried to join/include nested users within teams/projects and users which have an access to the password with in such way (writing from what I remember):
What I tried
# secret_policy.rb
class SecretPolicy < ApplicationPolicy
class Scope < Scope
def resolve
test = scope.includes(:projects => {:teams => :users}).includes(:teams =>
{:users}).includes(:users)
test.where(projects: {teams: {users: {id: user}}})
.or(test.where(teams: {users: {id: user}}).or(test.where(users: {id: user})))
end
end
# [...]
end
and then in controller:
# secret_controller.rb
class SecretController < ApplicationController
[...]
def show
#secret = policy_scope(Secret).find(params[:id])
end
[...]
end
which was supposed to join all related entities to Secret model and do nested queries but what was happening at the end was that even tho includes worked properly including all needed data, at the end where clause was using the same join name for running a query: users which is wrong because different join names containing needed data should be called.
More details:
I have a structure like so:
# user.rb
class User < ApplicationRecord
has_and_belongs_to_many :teams
has_and_belongs_to_many :secrets
end
# secret.rb
class Secret < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :teams
has_and_belongs_to_many :users
has_and_belongs_to_many :projects
end
# team.rb
class Team < ApplicationRecord
belongs_to :project
belongs_to :user
has_and_belongs_to_many :users
has_and_belongs_to_many :secrets
end
# project.rb
class Project < ApplicationRecord
belongs_to :user
has_many :teams
has_and_belongs_to_many :secrets
end
I'm kinda new to Rails (ActiveRecords) so my mapping might be a bit wrong. Here is what it supposed to be just to make it clear:
User:
can be owner of passwords
can have access to passwords given by other users
can belong to teams
can belong to projects
Secret:
can belong to 1 user
can be shared with many teams
can be shared with many users (not including owner. Owner has an access already)
can be shared with many projects
Team:
can belong to 1 project
belongs to only 1 user
many users can join the team
many secrets can be shared within the team
Projects:
belongs to only 1 user
can have many teams (uniquely)
many secrets can be shared within the projects
I found many resources talking about the case when you can do it with nested relations but usually it was done with only 1 resource being in where clause, so there was no issue with incorrectly used joins. I was about to give up and try to rewrite is in native SQL, but I believe there must be something.

Rails association in concerns

I am using concerns for my rails application. I've different kind of users so I have made a loggable.rb concern.
In my concern I have
included do
has_one :auth_info
end
because every of my user that will include the concern will have an association with auth_info table.
The problem is, what foreign keys I need to put in my auth_info table?
E.G
I've 3 kind of users:
customer
seller
visitor
If I had only customer, in my table scheme I would have put the field
id_customer
but in my case?
You can solve this with polymorphic associations (and drop the concern):
class AuthInfo < ActiveRecord::Base
belongs_to :loggable, polymorphic: true
end
class Customer < ActiveRecord::Base
has_one :auth_info, as: :loggable
end
class Seller < ActiveRecord::Base
has_one :auth_info, as: :loggable
end
class Visitor < ActiveRecord::Base
has_one :auth_info, as: :loggable
end
Now you can retrieve:
customer.auth_info # The related AuthInfo object
AuthInfo.first.loggable # Returns a Customer, Seller or Visitor
You can use rails g model AuthInfo loggable:references{polymorphic} to create the model, or you can create the migration for the two columns by hand. See the documentation for more details.
Since user has roles 'customer', 'seller', 'visitor'.
Add a column called role to Users table.
Add a column called user_id to auth_infos table.
class AuthInfo < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :auth_info
end
you can do
user = User.first
user.auth_info
Now you a additional logic to your concerns.

Model spread on several tables

Here is what I'm attempting to do:
class Account < ActiveRecord::Base
def name
end
end
class TypeA < Account
end
class TypeB < Account
end
Where TypeA and TypeB are stored on two distinct tables and Account acts pretty much as an abstract interface (with no table associated). They both have large number of entris and large number of fields so I want to keep them separated. Is there a way to go for this ?
(The exemple above does not work as a table for account is expected btw).
UPDATE
Now, if I use modules (as suggested in the answers), that raises another problem:
Let's say I have
class Transaction < ActiveRecord::Base
belongs_to :account, :polymorphic => true
end
where account can be TypeA or TypeB. I get the following misbehavior:
i = TypeA.new(:name => "Test")
t = Transaction.new(:account => i)
t.account.name
>> nil
which is not what I want as account.name should return "Test". How to get this?
Use module instead. You have shared behavior between those two models that you want to share. That's a great use-case for modules.
# inside lib/account.rb
module Account
# ...
def name
# code here
end
# ...
end
# inside app/models/type_a.rb
class TypeA < ActiveRecord::Base
include Account
end
# inside app/models/type_b.rb
class TypeB < ActiveRecord::Base
include Account
end

Design considerations for creating associated records on Devise User object on registration

I'm using Devise, and for each User account created I want to generate a relationship where:
class User < ActiveRecord::Base
belongs_to :business
end
class Business < ActiveRecord::Base
has_many :users
has_one :apt_setting
has_many :hours, :as => :hourable
end
class ApptSetting < ActiveRecord::Base
belongs_to :business
end
So upon registration an associated Business object is created, and with each Business object an associated ApptSettings and BusinessHour object is created.
I currently have this implemented like this:
class Admin
before_create :create_associated_records
def create_associated_records
# create the associated business object
business = Business.create(:business_name => business_name, :subdomain => subdomain, :initial_plan => initial_plan)
# retrieve the id of the new business object
self.business_id = business.id
# create the associated records
BusinessHour.default_values(business_id)
ApptSetting.default_values(business_id)
end
end
class ApptSetting < ActiveRecord::Base
belongs_to :business
def self.default_values(business_id)
# ... create record with default values
end
end
class BusinessHour < Hour
belongs_to :hourable, :polymorphic => true
def self.default_values(business_id)
# ... create record with default values
end
end
This does work, but does it seem like the best design?
One alternative I'm considering is handling removing Admin -> create_associated_records, and instead do that work in Users::Accounts::RegistrationsController where I override the 'create' method. There I could build all the associated records, set :accepts_nested_attributes where appropriate, then call 'save' on the Business object, which should then cause all the associated records to be generated.
Thoughts on the best design, or any other ideas?
you don't need the default_values methods. In your create_associated_records you can change those calls to:
ApptSetting.create(:business_id => business_id)
Don't override the create method. before_create callbacks are a better way to go. In either case, If a business has many users, do you really want to create a new business every time a new user is created? How does a second user ever get added to a business? add something like,
def create_associated_records
return unless self.business_id.nil?
....
Also where are the business_name, subdomain, and initial_plan variables coming from in your method? Do you have them as attributes of the admin user? Seems like they should be only values of the business.
I think the biggest question here is, does a user really need a business in order to exist? Why can't the user just create their Business after they create their account?
** Edit: Being more clear / cleaner version using rails association methods:
class Admin
before_create :create_associated_records
private
def create_associated_records
return unless self.business_id.nil?
self.create_business
self.business.create_appt_setting
self.business.hours.create
end
end

Creating associations by using checkboxes

A User can only have two types of Subscriptions: DailySubscription and WeeklySubscription. When the user is at the new and edit action, I'd like them to check off either of the subscriptions they would like to get.
I'm comfortable using nested fields (as per Ryan Bates' screencast here) but I think when I add inheritance, it really complicating matters. Is there a better way?
class User < ActiveRecord::Base
has_many :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
# type field is defined in the migration for Single Table Inheritance
end
class DailySubscription < Subscription
# Business logic here
end
class WeeklySubscription < Subscription
# Different business logic here
end
My initial efforts with the controller are wacky:
class UsersController < ApplicationController
def new
#user = User.new
# I can't use #user. subscriptions.build as Rails doesn't
# know what type of model to add!
#user.subscriptions = [DailySubscription.new, WeeklySubscription.new]
end
...
end
I think I am conceptually really missing something here but I can't figure it out. Help!
Judging from your description, your user has only two possible subscription choices: daily and/or weekly. Therefore you dont need to have a has_many association because two has_ones would suffice(note polymorphic subscribeable below:
class User < ActiveRecord::Base
has_one :daily_subscription, :as => :subscribeable
has_one :weekly_subscription, :as => :subscribeable
end
class Subscription < ActiveRecord::Base
belongs_to :subscribeable, :polymorphic => true
# type field is defined in the migration for Single Table Inheritance
end
class DailySubscription < Subscription
# Business logic here
end
class WeeklySubscription < Subscription
# Different business logic here
end
furthermore for the controller you just need to initialize User. Upon initialization, #user.daily_subscription and weekly_subscription will be null as determined by .blank? method. When you go ahead and create the user in the create method, you will need to populate these fields with instances of corresponding subscriptions.
class UsersController < ApplicationController
def new
#user = User.new
# bam -- youre done.
end
...
end

Resources