Single Table Inheritance with Conditions - ruby-on-rails

I have model User and model Recruiter. Currently, these are two separate tables, but I want to make them one.
Current:
User: id, username, password, name
Recruiter: id, user_id
Ideal:
User: id, username, password, role (recruiter, admin)
I understand the basics of STI. What I'm wondering is, when I perform methods on the new Recruiter controller (that inherits from User) how do I make sure all my methods are calling on users that are only a recruiter? Thus, queries along the lines of... SELECT * FROM users WHERE role = 'recruiter' for everything.

That is something rails takes care of for you, out of the box. You do not have to manually query on a particular type of user, just query on the right model.
I must also mention that by default rails assumes that your sti_column is called type, but can be overridden to role easily.

Let's admit you have your 2 classes:
class User < ActiveRecord::Base
end
class Recruiter < User
end
Rails will automagically add a type column in the users table so that in your controller, if you do something like this:
class RecruitersController < ApplicationController
def index
#recruiters = Recruiter.all
end
end
Rails will automatically fetch the records with type = 'Recruiter' and you don't even have to set this manually. If you do:
Recruiter.new(name: 'John').save
A new User will be created in database with the field type set to 'Recruiter'.

you would define your models something like this:
class User < ActiveRecord::Base
...
end
class Recruiter < User
...
def initialize
# ... special initialization for recruiters / you could put it here
super
end
...
end
and to create a new recruiter, you would do this:
Recruiter.create(:name => "John Smith")
and because of the type attribute in the STI user table (set to 'Recruiter'), the record will be for a recruiter.
You could put the special initialization for the STI models either in the model's initializer, or in a before filter with a if-cascade checking the type.
An initializer is probably much cleaner.

Have you tried has_one association?
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one

Related

Rails application with legacy database

I'm building an API that will look for data in a "non-rails-database". It already exists and was not created with the rails project. What is the best approach to use in this case ? Let's say I have a table called User in this database, I would like to be able to do something like this in the controller: User.last or User.find(id). It would be good, because I have to send back to the frontend as a JSON.
Should I use PORO Models ? How would it be ?
Create your models then set table name and primary key explicitly, this helps you call active record methods in a controller
class User < ApplicationRecord
self.primary_key = 'user table primary key'
self.table_name = 'user table name'
end
ref http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/PrimaryKey/ClassMethods.html
http://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-table_name-3D
I would personally prefer to have them in PORO. For me, it makes things clear and gives more flexibility.
For an example, I would have a seperate folder called api_models in the app folder and have the classes there.
First, it may feel like you are duplicating the code, but it will give you an object to work with when API needs changes.
E.g
ApiModel::User
...
end
also,
if you wanting be bit more advance and thinking about versioning (if not you should ;) etc, I would recommend a gem like grape, because that handles lot of api related things and all you need to focus is the code.
I would create custom methods that contain custom DB queries that override ActiveRecord methods. Because model classes inherit from ActiveRecord, if you define a method in the model class with the same method name as the ActiveRecord method, then when you call it, the model class will be called. You can still call the ActiveRecord method if you call super
So, assuming you already setup the connection to the DB, you can do something like:
class User < ApplicationRecord
def self.find(id)
sql = "Select * from ... where id = ..."
records_array = ActiveRecord::Base.connection.execute(sql)
end
end
So when you call find it will call the User find, and not the ActiveRecord find. You can also define a custom method so that you don't override the ActiveRecord find:
class User < ApplicationRecord
def self.new_find(id)
sql = "Select * from ... where id = ..."
records_array = ActiveRecord::Base.connection.execute(sql)
end
end
Not sure if its the best approach but its an approach :)

rails active record has_many foreign key after custom function

Consider following models
class User < AR
has_many :resources
end
class Resource < AR
belongs_to :user
end
I have a requirement where the foreign key is saved after applying some function on it. So the value of user_id in resources table does not match id in users table but it can be calculate again from id.
How can I define the association? Let's say that function is dummy_func().
Since belongs_to returns class instance instead of association, you can define methods in Resource class
class Resource < ApplicationRecord
def user
User.find(user_id)
end
def user=(user)
user_id = user.id
end
end
Similar for has_many result in user can be achieved by creating common relation in resources method
class User < ApplicationRecord
def resources
Resource.where(user_id: id)
end
end
So, if you use this code, you can replace any ids in Resource model, and behavior will exactly same as in belongs_to (Maybe there is some difference in depths). And you can achieve very similar behavior in User model, by writing methods by yourself.
Perhaps you can you can use a callback in order to modify the current user_id somehow before saving it: callbacks.
I'd suggest something like :before_save or something of that nature where you define how you want the user_id to be modified in the resources table and then have a way of decrypting it as well.
Maybe you can use an encryption gem to encrypt and decrypt your attribute like attr-encrypted.
Hope this helps a bit!
In the User model, you can override the setter. If you want to encrypt and decrypt the user ID (using attr_encrypted)...
You could try something like this:
attr_encrypted :id, key: ENCRYPTION_KEYS[:value]
def id=(value)
send("encrypted_value=", encrypt(:id, value))
instance_variable_set(:#id, value)
end
Then you can make a method that decrypts the ID
def decrypted_id
decrypt(:id, encrypted_value)
end
Now, when the User is created, the database will set the ID as usual. But it will also create an encrypted_value which stores the id as an encrypted ID. You can use this encrypted value around your app to keep the database ID secret from the interface.
Here is an example in console...

Creating subclasses using Single table Inheritance (STI)

I have the class User, and subclasses Admin and Student.
Student should have additional dedicated columns. Please let me know how I can do this using STI in Ruby on Rails.
Thanks!
Also, how do I populate the users table?
In Rails, typically you'd have type column on the User class. Now in your subclasses you'd inherit from the User class as such:
class User
end
class Admin < User
end
class Student < User
end
This way you can take advantage of the Rails STI and still be able to flexibly create methods for your subclasses.
Find more information here
However to keep it a bit organized, you could put the subclasses in a folder under your models, as such
#models/users/admin.rb
module Users
class Admin < User
end
end
#model/users/student.rb
module Users
class Student < User
end
end
Now to use your classes, you'd do Users::Student.find(id)
UPDATE
In response to the comment, I think for the columns that would be specific to the student, you'd be better served by an association, say Student.has_one :grade or something of sorts, this way you'd have successfully abstracted your user object to deal with the common User methods. But to create a row for Student and Admin
You could do Users::Student.create(params) or Users::Admin.create(params) and Rails knows how to deal with the STI

Create User Account Settings Page in Ruby on Rails with devise

I am new to Ruby on Rails and I have created a project that contains a User table (generated by devise) and a AccountSetting table that contains user specific account settings (this table has a foreign key that relates to the id in the User table thus each User has zero or one AccountSettings). I have my seed data working fine, and I can seed the database with users that have user specific account settings. The User table is related to the AccountSetting table with a "has_one :accountsetting" and the AccountSettings table "belongs_to :user". This all works and makes sense. However, I have a method called "show_user_setting" in my UserSettings controller, and I do not know how to ONLY SHOW the account settings for that specific authenticated user.
So, how can I only display the user setting for the currently logged in user? Again, I am using devise.
My general idea of how to do this would be something like this. However I know this is incorrect, but for the purpose of an explanation, here it is.
def show_user_setting
#setting = AccountSetting.find(current_user)
end
My idea is that the #setting will contain the setting for the currently logged in user. Thanks in advance!
You should do this:
#app/models/account_setting.rb
class AccountSetting < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :account_setting
end
This will allow you to call the following:
#setting = current_user.account_setting
Our Setup
For what it's worth, we do something similar:
#app/models/user.rb
class User < ActiveRecord::Base
before_create :build_profile #-> builds a blank profile on user create
has_one :profile
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
end
This allows us to put all sorts of different options inside the profile model (we have homepage etc):
The important thing to note here is that the above allows you to delegate various methods to the profile model, allowing you to call the following:
current_user.profile_name
current_user.profile_signin_redirect?
current_user.profile_avatar
etc
Have you tried
def show_user_setting
#setting = AccountSetting.find_by(user_id: current_user.id)
end
The way .find() works is it searches the model for the id passed. So, the way you currently have it is your going to try to search for the id of the model, when you want to find the foreign key. So use Model.find_by(column_name: param). You'll what to change user_id: to the column name of what you're storing the foreign key in, I'm just assuming it's something similar to that.
I'm guessing the show_user_setting function is part of a controller, if it is on a model then read this: accessing devise current_user within model
to set the #setting variable you should be able to do this
#setting = AccountSetting.find(user_id: current_user.id)
or
#setting = AccountSetting.find(user: current_user)

Rails 4 - Mapping a model dynamically on two different database tables

I have a multi domain app talking to a legacy database.
In that DB I have two tables with different names, lets call them USER_A and USER_B. Their structure and data types are exactly the same, the only difference is that they get their data from different domains.
Now, I would like to have a single scaffold (model/controller/view) that, depending on the domain, maps to the right DB table.
Domain A would work with a model/controller called User which maps internally to the db table USER_A, and Domain B would work with the same model/controller User but maps to the table USER_B.
I would also like to use resource :user in my routes to access the model the rails way.
So somehow I need to overwrite the model on initialization but I am not quite sure how to go about it.
How would one go about this using Rails ActiveRecord?
I don't have a multitable DB ready to test with, so this is an educated guess at the solution:
# respective models
class User < ActiveRecord::Base
end
class DomainAUser < User
self.table_name = "USER_A"
end
class DomainBUser < User
self.table_name = "USER_B"
end
# controller
def set_user
#user = if request.subdomain(0) == "DomainA"
DomainAUser.find(params[:id])
else
DomainBUser.find(params[:id])
end
end
Edit: Here's an alternative bit of metaprogramming hackery which does the subclass instantization within the parent class itself. Tested and working.
I really wouldn't want to maintain something like this though.
# model
class User < ActiveRecord::Base
def self.for_domain(domain_suffix)
class_eval "class DomainUser < User; self.table_name='user_#{domain_suffix}'; end"
"User::DomainUser".constantize
end
end
# controller
User.for_domain("a").new

Resources