Model spread on several tables - ruby-on-rails

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

Related

Same Model with different columns 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

Rails don't save if duplicate

I have a somewhat complex Rails model setup that I'll try to simplify as much as possible. The goal of this setup is to be able to have objects (Person, Pet) that are long-lived, but with relationships between them changing each year via TemporalLink. Basically, I have these models:
class Person < ActiveRecord::Base
include TemporalObj
has_many :pet_links, class_name: "PetOwnerLink"
has_many :pets, through: :pet_links
end
class Pet < ActiveRecord::Base
include TemporalObj
has_many :owner_links, class_name: "PetOwnerLink"
has_many :owners, through: :owner_links
end
class PetOwnerLink < ActiveRecord::Base
include TemporalLink
belongs_to :owner
belongs_to :pet
end
and these concerns:
module TemporalLink
extend ActiveSupport::Concern
# Everything that extends TemporalLink must have a `year` attribute.
end
module TemporalObj
extend ActiveSupport::Concern
# Everything that extends TemporalObj must have a find_existing() method.
####################
# Here be dragons! #
####################
end
The desired behavior is:
When creating a TemporalObj (Pet, Person):
1) Check to see if there is an existing one, based on certain conditions, with find_existing().
2) If an existing duplicate is found, don't perform the create but still perform necessary creations to associated objects. (This seems to be the tricky part.)
3) If no duplicate is found, perform the create.
4) [Existing magic already auto-creates the necessary TemporalLink objects.]
When destroying a TemporalObj:
1) Check to see if the object exists in more than one year. (This is simpler in actuality than in this example.)
2) If the object exists in only one year, destroy it and associated TemporalLinks.
3) If the object exists in more than one year, just destroy one of the TemporalLinks.
My problem is I have uniqueness validations on many TemporalObjs, so when I try to create a new duplicate, the validation fails before I can perform any around_create magic. Any thoughts on how I can wrangle this to work?
You can (and should) use Rails' built-in validations here. What you've described is validates_uniqueness_of, which you can scope to include multiple columns.
For example:
class TeacherSchedule < ActiveRecord::Base
validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
end
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of
In response to JacobEvelyn's comment, this is what I did.
Created a custom validate like so
def maintain_uniqueness
matching_thing = Thing.find_by(criteria1: self.criteria1, criteria2: self.criteria2)
if !!matching_thing
self.created_at = matching_thing.created_at
matching_thing.delete
end
true
end
Added it to my validations
validate :maintain_event_uniqueness
It worked.

Rails Validation of a Group of Objects

I have two objects, Project and User, which are joined by an object called ProjectAssignment. The ProjectAssignments object has an additional field: project_role. Models shown below.
class Project < ActiveRecord::Base
#relationships
has_many :project_assignments
has_many :users, :through => :project_assignments
end
class ProjectAssignment < ActiveRecord::Base
belongs_to :project
belongs_to :user
belongs_to :project_role
end
class User
has_many :project_assignments
has_many :projects, :through => :project_assignments
end
I have to validate that for a given project, there is exactly one ProjectAssignment with a project_role of "Principal Investigator" at any time. I'm a little unsure how to write a validation in the ProjectAssignment model. If I unset the current PI first then there is less than 1 Principal Investigator and if set a user to Principal Investigator before unsetting the other there is more than 1.
class ProjectAssignment
validates :allow_exactly_one_pi
def require_exactly_one_pi
if self.project_role.name == 'Principal Investigator' and other_princ_inv_exists
#more than one principle investigator set => error
elseif was_principle_investigator
#no principle investigator set => error
end
end
end
Any suggestions how this should be handled?
Very nice problem to chew on.
First you have to expose something about one model to another, there is a specific kind of role (principal investigator), and ProjectAssignment must be aware of that 'special case' role. But! it should be the project role that keeps track of that special status, so I'd add a method to the ProjectRole model:
ProjectRole < ActiveRecord::Base
def ispi?
self.name == 'Principal Investigator'
end
end
Then you have to figure out how to iterate over all project_asssignments and determine if any of them are a principal investigator. You have to access a Class method from an instance of a project_assignment.
class ProjectAssignment < ActiveRecord::Base
validate :there_can_only_be_one_principal_investigator
def there_can_only_be_one_principal_investigator
error = false
self.class.where('project_id = ?',self.project_id).each do |p|
if p.project_role.ispi?
error = true
break
end
end
if error
#whatever
end
end
end
Now you have to change your associations, ProjectAssignment can only have ONE role, so
class ProjectAssignment < ActiveRecord::Base
belongs_to :project
belongs_to :user
has_one :project_role
end
So now you are assured you cannot add a ProjectAssignment with project_role == Principal Investigator, if a Principal Investigator already exists for that Project.
What about an update, you update a ProjectAssignment with the project_role == PI, and there already is another ProjectAssignment with a PI, the validation will catch that.
Now how do you assure there is at least ONE PI? I think what that means is the FIRST ProjectAssignment for any one Project MUST be a PI. This is where you have to be a little hackish, you have to expose knowledge about the ProjectRole model directly in the ProjectAssignment model.
validate :there_must_be_at_least_one_principal_investigator
def there_must_be_at_least_one_principal_investigator
if self.class.where('project_id = ?', self.project_id).count() == 0 AND !self.project_role_id == 1
#error
end
end
I don't really like this solution, because the fact that the PI roles's id is 1 (or whatever it is), is hardcoded in another Model!! To make it a little LESS objectionable, you could add a Class method to the ProjectRole model
class ProjectRole < ActiveRecord::Base
def self.piid
1 # or whatever it is
end
end
Then do this:
def there_must_be_at_least_one_principal_investigator
if self.class.where('project_id = ?', self.project_id).count() == 0 AND ! self.project_role_id == ProjectRole.piid
#error
end
end
Now, how do you change the principal investigator? You'll have to do that in a seperate action, i.e.
class ProjectAssignmentController < ApplicationController
def change_pi
#proj_assignment1 = ProjectAssignment.find(params[:orig_pi_id])
#proj_assignment2 = ProjectAssignment.find(params[:new_pi_id])
#proj_assignment1.project_role_id = params[:new_role_for_orig_pi].to_i
#proj_assignment1.save :validate=>false # it's OK, you're taking care of it below
#proj_assignment2.project_role_id = ProjectRole.piid
#proj_assignment2.save
end
end
Here is a completely different approach, in summary format:
1) When you Create a new Project, ask for the User who should be the PI, and build the first ProjectAssignment record, that eliminates the need to validate that there is at least one PI.
2) In ProjectController and in your projects edit view, create a 'change PI' interface of some kind, in that interface you'd need to know the user_id of the user that becomes the new PI, and the project_role_id that the current PI needs to be assigned if he stays on the project, if project_role_id is nil, that means that user should be deleted from the project.
This approach would eliminate validations altogether!
The first answer was a very fun exercise, and I hate to suggest someone change their entire approach, I'd rather just answer the question asked the best I can. But after seeing how complex the 'validations' became, I think it would be best not to rely on them in your case, you just have to CODE your app to insure your conditions are met.
HTH

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