I’m creating a chore tracker app as my first Rails project and I’m wondering if the associations I've created make sense or could be improved. Here are the details of the app:
A user makes a chore list and becomes the “owner”(i.e. “admin”) of
that list.
The owner can create/edit chores for the list. They can also “approve" other users to complete tasks on the list. These users can ONLY complete tasks.
The owner, along with the usual admin abilities, can also complete
tasks on a list that they own.
Owners can own multiple lists. Users can be approved to complete
tasks on multiple lists.
And here are the relationships I’ve roughed out that I’m looking for feedback on:
(Model)User
has_many :lists
has_many :owners, class_name: “List”, foreign_key: “list_id"
has_many :chores, through: :lists
(Model)List
has_many :users
has_many :chores
belongs_to :owner, class_name: “User”, foreign_key: “owner_id"
(Model)User_List
belongs_to :user
belongs_to :list
(Model)Chore
belongs_to :list
has_many :users, through: :lists
Any red flags? Thank you in advance!
I would do something like this:
# user.rb
class User
has_many :lists_users
has_many :lists, through: :lists_users
has_many :chores, through :lists
end
# list.rb
class List
has_many :lists_users
has_many :users, through: :lists_users
has_many :chores
end
# lists_user.rb
class ListsUser
belongs_to :user
belongs_to :list
# field (Representational, add it in database)
:user_role
- member (can view)
- collaborator (can mark complete)
- owner (can do everything)
end
# chore.rb
class Chore
belongs_to :list
end
NOTE: I changed the association model to ListsUser as per rails naming conventions.
Please let me know if i missed something.
Related
I'm currently setting up a feed from scratch where a user subscribes to multiple message boards and would receive all of the posts created under that board. This is how I have my code written so far:
class User < ApplicationRecord
has_many :subscriptions, dependent: :destroy
has_many :boards, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :user
belongs_to :board
end
class Board < ApplicationRecord
has_many :subscriptions, dependent: :destroy
has_many :subscribers, through: :subscriptions, source: :user
end
class Post < ApplicationRecord
belongs_to :board
belongs_to :user
end
When it comes time to display the posts is where I'm having problems as far as the proper approach. Would the simple solution be to setup another HMT association in the user model as so?
has_many :subscribed_posts, through: :boards, source: :posts
One immediate flaw I see in my approach is that the user would get their posts to show up including those of other users. Would the better solution be to create some sort of SQL query?
Yes, you can build another association to retrieve posts you need, but as you mentioned that would return also the users' posts too, so you have to put some conditions to prevent such behavior.
has_many :subscribed_posts, through: :boards, source: :posts, conditions: {["user_id != ?", id] }
I've been going back and forward on this and I would like some advices.
I have "User" that can be part of many "Organizations", and for each one they can have many "Roles". (actually I have this scenario repeated with other kind of users and with something like roles, but for the sake of the example I summed it up).
My initial approach was doing a Table with user_id, organization_id and role_id, but that would mean many registers with the same user_id and organization_id just to change the role_id.
So I thought of doing an organization_users relation table and an organization_users_roles relation. The thing is, now I don't exactly know how to code the models.
class Organization < ActiveRecord::Base
has_and_belongs_to_many :users, join_table: :organization_users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :organizations, join_table: :organization_users
end
class OrganizationUser < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :organizations
has_many :organization_user_roles
has_many :roles, through: :organization_user_roles
end
class OrganizationUserRole < ActiveRecord::Base
has_and_belongs_to_many :roles
has_and_belongs_to_many :organization_users
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :organization_user_roles
end
If for example I want to get: ´OrganizationUser.first.roles´ I get an error saying: PG::UndefinedTable: ERROR: relation "organization_user_roles" does not exist
How should I fix my models?
You should use a much simpler approach. According to your description, Roles is actually what connects Users to Organizations and vice-versa.
Using the has_many and has_many :through associations, this can be implemented like the following:
class User < ActiveRecord::Base
has_many :roles, inverse_of: :users, dependent: :destroy
has_many :organizations, inverse_of: :users, through: :roles
end
class Organization < ActiveRecord::Base
has_many :roles, inverse_of: :organizations, dependent: :destroy
has_many :users, inverse_of: :organizations, through: :roles
end
class Role < ActiveRecord::Base
belongs_to :user, inverse_of: :roles
belongs_to :organization, inverse_of: :roles
end
If you wish to preserve roles when you destroy users or organizations, change the dependent: keys to :nullify. This might be a good idea if you add other descriptive data in your Role and want the role to remain even though temporarily vacated by a user, for example.
The has_many :through association reference:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
To add to jaxx's answer (I upvoted), I originally thought you'd be best looking at has_many :through:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :positions
has_many :organizations, through: :positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
#columns id | user_id | organization_id | role_id | etc | created_at | updated_at
belongs_to :user
belongs_to :organization
belongs_to :role
delegate :name, to: :role #-> #position.name
end
#app/models/organization.rb
class Organization < ActiveRecord::Base
has_many :positions
has_many :users, through: :positions
end
#app/models/role.rb
class Role < ActiveRecord::Base
has_many :positions
end
This will allow you to call the following:
#organization = Organization.find x
#organization.positions
#organization.users
#user = User.find x
#user.organizations
#user.positions
This is much simpler than your approach, and therefore has much more ability to keep your system flexible & extensible.
If you want to scope your #organizations, you should be able to do so, and still call the users / positions you need.
One of the added benefits of the code above is that the Position model will give you an actual set of data which can be shared between organizations and users.
It resolves one of the main issues with jaxx's answer, which is that you have to set a role for every association you make. With my interpretation, your roles can be set on their own, and each position assigned the privileges each role provides.
If the user can have many Roles for a single organisation,
and OrganizationUser represents this membership,
than, yes, you need another table for organization_user_roles.
You need to explicitly create it in the database (normally with a migration)
To not get confused, try to find a nice name for OrganisationUser, like employment, membership, etc.
I'm stuck on this:
class Worker < ActiveRecord::Base
has_many :skills
has_many :jobs, through: :skills
..
end
class Skill < ActiveRecord::Base
belongs_to :worker
has_many :jobs
..
end
class Job < ActiveRecord::Base
has_many :skills
has_many :workers, through: :skills
..
end
What I'm trying to do is set up a many to many between Skill and Job inside of the `has_many' through relationship?
My question has three parts
Is this possible - using the has_many jobs rather than belongs_to jobs.
If it can be done and the code is wrong, how do I fix it?
How can I create Worker, Skill and Job records? (looking for syntax)
This is a picture (of sorts) of what I'm trying to do, hope it helps... :(
You're not giving active record enough information about your relationships. Every :has_many should have a corresponding :belongs_to so that active record knows which table holds the foreign key for each association. Notice that you only have one :belongs_to for three relationships. That smells.
As for fixing the problem, you have at least 2 options:
add :has_and_belongs_to_many associations
use explicit join tables
My preference is for the latter option. Being forced to name join tables often clarifies the nature of a relationship. On the flip side, I've found that :has_and_belongs_to_many is often too implicit and ends up making my designs more obscure.
Explicit join tables
You might setup your relationships like this (untested):
class Assignment < ActiveRecord::Base
belongs_to :worker
belongs_to :job
end
class Qualification < ActiveRecord::Base
belongs_to :worker
belongs_to :skill
end
class Worker < ActiveRecord::Base
has_many :qualifications
has_many :skills, through: :qualifications
has_many :assignments
has_many :jobs, through: :assignments
..
end
class Skill < ActiveRecord::Base
has_many :qualifications
has_many :workers, through: :qualifications
has_many :jobs
..
end
class Job < ActiveRecord::Base
has_many :skills
has_many :workers, through: :assignments
..
end
By making the relationships more explicit I think the model is clearer. It should be easier to troubleshoot from here.
EDIT:
If you need to do a traversal like Job.find(1).qualified_workers try making the following adjustment to the above model:
class Job
has_many :required_competencies
has_many :skills, through: :required_competencies
has_many :qualifications, through: :skills
has_many :qualified_workers, through: qualifications, class_name: :workers
end
class RequiredCompetency
belongs_to :job
belongs_to :skill
end
This is explicit about each traversal and names it. If you find these paths through your system are getting really long, I'd consider that a smell. There might be a more direct way to fetch your data or perhaps a better way to model it.
I have models User, Team, Document. There's a many-to-many relationship between Users and Teams, and a many-to-many relationship between Teams and Documents, using join tables called TeamMembership and TeamDocument respectively.
The relationships in my models look like this:
class Document < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :teams, through: :team_documents
end
class User < ActiveRecord::Base
has_many :team_memberships, dependent: :destroy, foreign_key: :member_id
has_many :teams, through: :team_memberships
has_many :documents, through: :teams
end
class TeamDocument < ActiveRecord::Base
belongs_to :team
belongs_to :document
end
class TeamMembership < ActiveRecord::Base
belongs_to :team
belongs_to :member, class_name: "User"
end
class Team < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :documents, through: :team_documents
has_many :team_memberships, dependent: :destroy
has_many :members, through: :team_memberships
end
The idea is that users can belong to many teams, a document can be associated with many teams, and users will only have access to documents that "belong" to at least one team that the user is a member of.
Here's the question: I can use User#documents to retrieve a list of all the documents that this user is allowed to view. But this will return duplicates if a document is viewable by more than one team which the user is a member of. How can I avoid this?
I know I can remove the duplicates after the fact with #user.documents.uniq, but as I will never want to include the duplicates in any case, is there a way I can just make #documents not include duplicates every time?
I don't have nested has_many :through like yours to test it, but I suspect using uniq option on your user association would help :
class User < ActiveRecord::Base
has_many :documents, through: :teams, uniq: true
end
You can add a default_scope on Document model:
class Document < ActiveRecord::Base
default_scope group: { documents: :id }
My app allows users to follow several different model types, including other users, organizations, products, etc. Each of these models has a has_many: :histories association. My goal is to compile all the :histories of the resources the current_user is following.
My models look like this:
class Follow < ActiveRecord::Base
belongs_to :user
belongs_to :followable, polymorphic: true
end
class History < ActiveRecord::Base
belongs_to :historical, polymorphic: true
end
class User < ActiveRecord::Base
has_many :follows
has_many :followed_resources, through: :follows, source: :followable
has_many :followed_histories, through: :followed_resources, source: :histories
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
class Product < ActiveRecord::Base
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
class Organization < ActiveRecord::Base
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
etc
My goal is to get the histories from all the current_user's followed resources like so:
#histories = current_user.followed_histories
But unfortunately, Rails does not allow us to traverse polymorphic association in a has_many: through relationship. Instead, it insists that we specify only a single association using the source_type option. For instance
has_many :followed_products, through: :follows, source: :followable, source_type: :product
Unfortunately, this approach won't work in this case, unless there's some way to recombine all the associations afterwards. For instance, if there was some way to do this:
has_many :followed_histories, through: {:followed_products, :followed_organizations, :followed_users}
Or perhaps there's yet another approach I'm not considering. I'm open to any suggestions, so long as the end result is a single array containing the combined histories of the followed resources.