Model style for Ruby on Rails application - ruby-on-rails

So currently my application has a model called Vendors. I want each Vendor to have one or many owners, one or many members, and then people who track or follow the Vendor. I'm trying to decide the best layout for this and I've messed around with a few things. All of the Owners, members, and followers would also be coming from my User model. Therefore I'm thinking of making one association table with booleans as to whether the User is a follower, owner, or member.
For clarification - Owner and member would be people who work at the vendor
schema.db
create_table "vendor_relationships", force: true do |t|
t.integer "user_id"
t.integer "vendor_id"
t.boolean "owner"
t.boolean "member"
t.boolean "follower"
t.datetime "created_at"
t.datetime "updated_at"
end
but I'm wondering if this is the best way to go about this when it comes to speed and/or quality of code. Previously I had created separate tables for vendor_owners, vendor_members, and vendor_followers. Then I associated them with a has_many :through relationship, but I'm stuck onto which way is the best.
They would all have separate logic, or rather separate permissions as to what they were allowed to view and edit with respect to the vendor. I looking at cancan and pundit for role based authorization, but I didn't think it really applied for this situation.
A role based authorization could work if I build the schema like this
create_table "vendor_relationships", force: true do |t|
t.integer "user_id"
t.integer "vendor_id"
t.string "role"
t.datetime "created_at"
t.datetime "updated_at"
end
And then check for possible user roles against the string but I'm not sure which of these is the better option.
EDIT 1:
I'm going with the t.string "role" route, but I'm wondering how I can write the code into my model so that the associations with role = "owner" would be accessible by doing Vendor.owners... etc with Vendor.members and Vendor.followers.
This is what my code looks like currently.
has_many :owners, through: :vendor_relationships, class_name: "User", -> { where role: owner }
This for each variation - owners, members, followers. I've also tried using
has_many :owners, through: :vendor_relationships, class_name: "User", conditions: => ['vendor_relationship.role = "owner"']
but everything is giving me syntax errors here. Would appreciate some help thanks.

I think you are on the right track. I would do the following:
Model your owners, members, and followers all as User, but then give them each a role. Start with a single role per user to keep it simple. See: https://github.com/ryanb/cancan/wiki/Role-Based-Authorization
In the many-to-many joins table, I would remove the booleans and instead let your association handle that logic.
has_many :owners, through: :vendor_members, class_name: "User", conditions: {"users.role = 'owner'"}
Then you can just interact with owners/members/followers like so: owners = vendor.owners
I would also consider changing the name of the vendor_members table since one of the types is also members. Maybe something like vendor_relationships

Related

Adding additional fields on top of an inherited model and expose all super and child class fields

My Goal:
I'm trying to create two different types of users, with different profiles.
Barber, Client, BarberProfile, and ClientProfile
I have my base User object that contains information like email, password, and all the other fields that Devise keeps track of.
I'd like the base User model to have one Profile that keeps track of all basic information that I want all my users to have. For instance, first_name, last_name, avatar, etc.
I'm using single table inheritance to create two different types of users: Client and Barber.
I want each of these types of users to have a base Profile associated with it, and then have additional fields that belong to a BarberProfile and a ClientProfile, respectively.
The BarberProfile will have things that the Barber needs, but the Client doesn't. For instance, a bio. The ClientProfile will have things the Client needs, but the Barber doesn't. For instance, hair_type.
What I currently have, and my problem:
As stated above, I've created a table for User and Profile. So I'm able to call user.profile.first_name. I created a BarberProfile and ClientProfile table in order to add the extra fields.
I'd like to just be able to reference user.profile.bio if the user type is Barber. But bio isn't a part of the base profile. So in this case I'd have to create an associated Profile and and associated BarberProfile to get everything I need. I could just do user.profile.first_name and user.barber_profile.bio, but it feels messy, and I'm making two different associations from essentially the same type of model. I feel like it should be a simple thing to just have the BarberProfile inherit all fields from Profile and add its own specific Barber fields on top.
How does one go about doing this in Rails?
Edit: One of the main reasons I want to do this is so I can update things like first_name and bio within the same form for a Barber. And similarly, a first_name and hair_type within the same form for a Client.
If you want to avoid using two associations on the users for Profile and Client/BarberProfile, I think you should make ClientProfile and BarberProfile extend Profile (single table inheritance) and each of them "has one :barber_profile_data" (I'm not sure how to call it). To fix the long method calls, you can use delegated methods.
class Barber > User
has_one :barber_profile
delegate :bio, to: :barber_profile
class Client < User
has_one :client_profile
delegate :first_name, to: :client_profile
class BarberProfile < Profile
has_one :barber_profile_data
delegate :bio, to: :barber_profile_data
class ClientProfile < Profile
has_one :client_profile_data
delegate :first_name, to: :client_profile_data
Then, when you do "#barber.bio", it should call, internally, "#barber.barber_profile.barber_profile_data.bio".
This sounds like a good use case for Multiple Table Inheritance. In MTI you use additional tables to decorate the base model.
The main advantage to MTI is that the clients and barbers tables can contain the specific columns for that type vs STI which requires you to cram everything into users by design (thats why they call it single table).
create_table "barbers", force: :cascade do |t|
# only the specific attributes
t.text "bio"
end
create_table "clients", force: :cascade do |t|
# only the specific attributes
t.string "hair_type"
end
create_table "users", force: :cascade do |t|
t.string "email"
t.string "first_name"
t.string "last_name"
# ... all the other common attributes
t.integer "actable_id"
t.string "actable_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
This is an example with the ActiveRecord::ActsAs gem.
class User < ApplicationRecord
actable
end
class Barber < ApplicationRecord
acts_as :user
end
class Client < ApplicationRecord
acts_as :user
end
Note that Barber and Client should not be true subclasses. ActiveRecord::ActsAs instead delegates to the "actable" class.
You can then do Barber.all or Client.all to fetch specific types or use User.all.map(:specific) to get decorated users of all types.

How to keep attributes from two different tables synchronized in RoR?

What I want is to be able to easily be able to find the team name associated with a membership without having to send another request to the database. My plan was to just add a team_name attribute to the Memberships table, but I can't think of a way to have this new attribute stay in sync with the name attribute in the Teams table. In other words, I want it to be the case that, if the owner of a team changes the team's name, then all memberships to that team will be updated (with the new team name) as well.
Here is what my setup looks like. I've fairly new to Rails.
/app/models/membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :team
end
/app/models/team.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :team
end
/app/db/schema.rb
ActiveRecord::Schema.define(version: 20161022002620) do
create_table "memberships", force: :cascade do |t|
t.integer "user_id"
t.integer "team_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "teams", force: :cascade do |t|
t.string "name"
t.integer "user_id"
end
end
If there is a better way to achieve what I am asking, then please let me know as well.
With this relational data your membership doesn't need a team name attribute - it is already available through the team association.
Generally there's no reason to keep data 'in sync' in this way unless you're performing some sort of computation. You don't need to store a name attribute on Membership - you can just use the existing one in Team.
I have seen people add duplicate database columns because they don't know how to traverse through associations. But unless you're using some noSql system, this isn't the 'right way' to do it - there is an underlying SQL API (through ActiveRecord) that performs lookups very efficiently.
in response to your comment. Do this:
class Membership < ActiveRecord::Base
def name
team.name
end
end

Associating multiple existing records

I'm creating an application where one user becomes the account_manager of an account. What I want to do is to add other users to the account. A user can only have one account but an account can have many users.
class Account < ActiveRecord::Base
has_many :users
belongs_to :account_manager, :class_name => 'User', :foreign_key => 'account_manager_id'
end
class User < ActiveRecord::Base
has_one :account
What I'm totally stuck on is having a place where the account manager can either select the user from a dropdown, type in their name, or use some other type of selection. If I try to do this in console each new user I add replaces the last instead of adding to it. here is my schema for accounts:
create_table "accounts", force: :cascade do |t|
t.integer "user_id"
t.integer "account_manager_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I've tried using collection_select but I think that is only for :though associations. I'm also thinking I probably need a join table but I don't know how to set it up. The thing that is tripping me up most is that I won't be creating new objects, I only want to add existing users to existing accounts. I'm just looking for someone who can talk through this with me.
In your User model:
class User < ActiveRecord::Base
belongs_to :account
end
While in your question, you are writing has_one. You can't write has_many in one model, and has_one in other model. There needs to be belongs_to in one model.
Edit:
The model that belongs_to, always saves the foreign keys in its table. So users would save account_id in it. In order to get all the users of an account, you would simply do:
Account.first.users # As an account `has_many` users

Rails two to many relationship

I have a problem that I solved in a certain way that is not satisfying.
I have a Game model and in a game there are always two teams involved that are part of Team model. I reference to these teams by ids team1_id and team2_id. From my views whenever I want to pull the entire Team record, I have to do a find every time.
I was wondering if there is any way to reference these two teams without going through a many to many relationship or is it the only way? It would be almost a 2-Many relationship, I know that doesn't exist but I would like to know the best way to solve this kind of problems.
Thank you,
This is a snapshot of my migrations:
create_table :games do |t|
t.datetime "time"
t.integer "team1_id"
t.integer "team2_id"
create_table :teams do |t|
t.references :city
t.references :user
t.string "name", :default => "", :null => false
The way you have it setup is the right way.
On the Game model make two team references, team1 and team2
class Game
belongs_to :team1, class_name: 'Team'
belongs_to :team2, class_name: 'Team'
end
Then you can just call team1 an team2 on the game instance and it will pull the teams for you.
game = Game.first
game.team1
game.team2
Or you can drop the team1 and team2 id's from the Game model and create a join table with games and teams, an you'd just call "game.teams"

How to access associated model through another model in Rails?

UPDATE (specific and more detailed previous version is below):
I'm developing a TV station web site. Here are requirements for my Program section:
Each Program has ONE Category.
Each Program has ONE Subcategory.
Each Category has MANY Subcategories
Each Category has MANY Programs.
Each Subcategory has ONE Category
Each Subcategory has MANY Programs.
I want to retrieve all these three models to be associated. For example, I should be able to retrieve below data from my views:
While:
p = Program.find(1)
p_cat = ProgramCategory.find(1)
p_subcat = ProgramSubcategory.find(1)
I should be able to retrieve and also EDIT these:
p.program_category
p.program_subcategory
or
program_category.programs
program_subcategory.programs
You can see what I tried below to achieve these requirements. You may recommend me a totally different way or fix my mistakes.
Thank you
============================================================
I have 3 models. They are supposed to be nested in eachother.
ProgramCategory > ProgramSubcategory > Program
Here are my codes:
ProgramCategory model:
has_many :programs
has_many :program_subcategories
ProgramSubcategory model:
belongs_to :program_category
has_many :programs
Program Model:
belongs_to :program_category
belongs_to :program_subcategory
As I create a new Program, I can set its Category and everything is fine. I can access them from both sides. For example,
program.program_category
gives me what I expected. and also
program_category.programs
gives me what I want to have, too.
BUT, -here comes the question-
When I try to access program.program_subcategory, I receive just a nil.
Eventhough my Subcategory's Category is set and my Program's Category is set too, why I can't access program.program_subcategory directly?
When I type program_category.program_subcategories, I receive all Subcategories owned by that Category. But I CAN NOT get Subcategories from directly a Program object.
My scheme is below. Any help is appriciated.
ActiveRecord::Schema.define(:version => 20120926181819) do
create_table "program_categories", :force => true do |t|
t.string "title"
t.text "content"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "program_subcategories", :force => true do |t|
t.integer "program_category_id"
t.string "title"
t.text "content"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "program_subcategories", ["program_category_id"], :name => "index_program_subcategories_on_program_category_id"
create_table "programs", :force => true do |t|
t.integer "program_category_id"
t.integer "program_subcategory_id"
t.string "title"
t.text "content"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "programs", ["program_category_id", "program_subcategory_id"], :name => "my_join1", :unique => true
end
The design is strange a bit. If you need nesting like
ProgramCategory > ProgramSubcategory > Program
then you need
class Program < ActiveRecord::Base
belongs_to :program_subcategory
end
class ProgramSubcategory < ActiveRecord::Base
belongs_to :program_category
has_many :programs
end
class ProgramCategory < ActiveRecord::Base
has_many :programs, :through => :program_subcategories
has_many :program_subcategories
end
This way when you create a program you can assign a subcategory to it. And this subcategory is already assigned to category, so you can access it via program.program_subcategory.program_category
And you do not need program_category_id foreign key in programs because program is not connected to category directly, but via subcategory.
UPDATE
Each Program has ONE Category.
Each Program has ONE Subcategory.
Each Category has MANY Subcategories
Each Category has MANY Programs.
Each Subcategory has ONE Category
Each Subcategory has MANY Programs.
Then I believe that my answer is still valid. You see, my structure is the same as your description except Each Program has ONE Category (because rails has no belongs_to through). you has one is actually belongs_to (because it can belong to only one).
But as soon as Each Program has ONE Subcategory and Each Subcategory has ONE Category program's subcategory's category will be the ONLY program's category. You can have p.program_category by defining a method on a Program class:
def program_category
program_subcategory.program_category
end
Now for the part of
I should be able to retrieve and also EDIT these:
p.program_category
Imagine you have a Program in subcategory Comedy from category Movies.
You say you want to be able to EDIT programs category directly (if I understood correctly), like this:
p.program_category = ProgramCategory.find_by_name("Sports")
But what you expect to be program's subcategory then? As soon as Sports have many subcategories? Do you expect it to be blank?
So in this design the only way to change program's category is to change program's subcategory:
p.program_subcategory = ProgramSubcategory.find_by_name("Tennis")
And this will manke program's category == Sports, because Tennis belongs to Sports.
Note: If you really want sometimes to change program's category directly, leaving its subcategory blank it requires another design of course. I do not think it is very difficult but it requires more work with less help from Rails AR Associations magic.

Resources