Ruby On Rails User Model for multiple types

2020-05-15 06:28发布

问题:

I am learning RoR coming from many years of c# and MSSQL.

I have picked a project to build a web site for my brother who is a rental property manager. I figured this should be fairly easy as the models should be straight forward, but it think I may be over thinking everything or I’m having trouble letting go of the ‘old’ way. Anyway here is the problem. I am starting off with just two models (User and Property) . The property model is easy, the user not so much. I figured that we have three types of users in the system. Tenants, Owners and Managers (my brother will be the only manager but I figured I would design it to grow) He manages properties for several owners each of whom can own many properties. Each property will have one owner, one tenant and one manger.

Tenants will be able to log in and just see the property they rent to maybe fill out a maintenance request or something like that…(no real requirement at this point to even give tenant a login to the system but I thought it would be a good exercise)

Same thing goes for owners, none of them really need access to the system (they hire my brother so they don’t have to be involved) but I thought it might be nice and again a good exercise.

I used the Nifty_generator to generate a user, which just gives email, password etc. I have extended it as follows…

class AddProfileDataToUsers < ActiveRecord::Migration
  def self.up
    add_column :users, :first_name, :string
    add_column :users, :last_name, :string
     add_column :users, :address1, :string
     add_column :users, :address2, :string
     add_column :users, :city,:string
     add_column :users, :state, :string
     add_column :users, :zip, :string
     add_column :users, :phone, :string
     add_column :users, :email, :string
     add_column :users, :user_type, integer
  end

  def self.down 
    remove_column :users, :first_name 
    remove_column :users, :last_name
   remove_column :users, :address1
   remove_column :users, :address2
   remove_column :users, :city
   remove_column :users, :state
   remove_column :users, :zip 
   remove_column :users, :phone 
   remove_column :users, :email 
   remove_column :users, :user_type
  end
end

Here is the code to create the Properties table

class CreateProperties < ActiveRecord::Migration
  def self.up
    create_table :properties do |t|
      t.string :address
      t.string :city
      t.string :type
      t.integer :beds
      t.float :baths
      t.float :price
      t.float :deposit
      t.string :terms
      t.string :laundry
      t.datetime :date_available
      t.integer :sqft
      t.integer :owner_id
      t.integer :manager_id
      t.integer :tenant_id
      t.timestamps
    end
  end

  def self.down
    drop_table :properties
  end
end

I added the following to the user model that was generated by the nifty_authentication generator

class User < ActiveRecord::Base

  #other stuff in the user model up here......
  validates_length_of :password, :minimum => 4, :allow_blank => true

  #this is the stuff that I have added to the user model
  has_many :managed_properties, :class_name => "Property", :foreign_key => "manager_id"
  has_many :owned_properties, :class_name => "Property", :foreign_key => "owner_id"
  has_one :rented_property, :class_name => "Property", :foreign_key => "tenant_id"

I then added this to the property model....

class Property < ActiveRecord::Base
    belongs_to :manager, :class_name => "User" #picked up by the manager_id
    belongs_to :owner, :class_name => "User"  #picked up by the owner_id
    belongs_to :tenant, :class_name => "User"  #picked up by the tenant_id
end

My question is, does this look like an acceptable way of modeling the situation I described?

Should I be using the single table inheritance and creating a tenant model; a manager model; and an owner model? The problem I saw with doing that was that a single user could be both a manager and an owner. This could be solved by then having a roles tables for the user where a user has many roles and a role has many users. I had also looked at a profile table with a one to one match with the user table and making this polymorphic but I didn't think that this situation really calls for that and it didn't solve the issue where a user can be an owner and a manager.....

This is when I started to think that maybe I was over thinking the problem and came up with what you see here.

I welcome any constructive comments you may have. Please keep in mind that I have never actually built anything in Rails and this is all a first attempt, one week ago I had never even installed rails on my computer.

I don't know if this matters but I figured that the admin / manager would be responsible for creating users. This will not be a self sign up type of site. The manager will add new owners when he signs up new owner, and the same will go for tenants. This will make it easier to determine the type of user he is creating.

Thanks for any insight you may have.

回答1:

FWIW, this looks fine to me. I might look at declarative_authorization to manage your roles, which might end up somewhat involved, particularly from the UI standpoint. Managing users with multiple roles seems more appropriate than STI in this situation, because, as you say, a user can be both manager and tenant, etc.

One approach to keeping the code for manager, tenant and owner separate, while allowing for the use of all three roles in a single instance, might be to dynamically include modules representing those roles based on what roles any given user has. You'd still have a base class of User, but instead of using STI, which would limit you to a single inheritance structure, you could mix in modules based on the roles each user has, which might simply pivot on what Properties belong_to a given user during initialization.

For this I might create a User factory that can inspect the user for the various roles it plays and extend the singleton class of that user with the corresponding module: extend with the Tenant module for users that have tenant properties, extend with the Manager module for users that have managed properties, etc.

From the standpoint of declarative_authorization, you could declare the role_symbols similarly based on whether associations like managed_properties were populated, for instance:

def role_symbols  
  @roles ||= {:manager => :managed_properties, 
    :tenant => :rented_property, 
    :owner => :owned_properties}.map do |k,v|
      k if !send(v).blank?
    end.compact
end

Or something similar. You'd probably want to set the roles AND include the corresponding module at the same time. Ruby and Rails gives you many options via metaprogramming techniques to dynamically decorate with the functionality individual models need. My approach may or may not be appropriate for your application, but there are countless other ways you could approach the problem and keep your code clean and DRY.

Overall it seems to me your data model is sound, and your instinct not to use STI to manage multiple roles is correct. It doesn't look to me like you've overthought it -- I think you're on the right track. It's actually pretty Rails-y for a first pass.

EDIT: You know, the more I think about it, I'm not sure what the benefit of keeping the manager/tenant/owner functionality in separate modules really is. In my former incarnation as a Java/C# guy I would have been all about the SRP/IOC and total separation of concerns. But in Ruby and Rails it's not nearly as big of a deal, since it's dynamically typed and coupling is not nearly as big, or at least the same sort, of concern that it is in statically-typed environments. You might be perfectly fine just putting all of the individual role functionality in the single User model and not worry with the modules, at least not yet.

I'm on the fence here, and would welcome input from others. To me one of the benefits to Ruby classes, as opposed to Java classes/packages or .NET classes/assemblies, is that you can always refactor as needed and not be nearly as concerned about which class, package, namespace, dll or jar is coupled to another, etc. I'm not saying SRP isn't important in Ruby, not at all. But I'm not nearly as paranoid about it as I used to be.

EDIT: Paul Russell makes an excellent point. I think you should seriously consider allowing multiple tenants/managers/landlords per property. In Rails this could be expressed through a relational table and a has_many :through association, plus STI to describe the different types of relationships. I also think it will be necessary to invert the relationship between User (as Tenant) and Property. A Property can have more than one Tenant, but a Tenant can't live at more than one property. (or maybe they can? Doesn't seem right, but...)

Maybe something like (this is very quick and dirty, so forgive any missed details please):

class PropertyRole < ActiveRecord::Base
  belongs_to :user
  belongs_to :property
end

class Managership < PropertyRole
  # Manager functionality here
end

class Ownership < PropertyRole
  # Owner functionality here
end

class User < ActiveRecord::Base
  belongs_to :residence, :class_name => 'Property', :foreign_key => 'residence_id'
  has_many :managerships
  has_many :ownerships

  has_many :owned_properties, :through => :ownerships, :classname => 'Property'
  has_many :managed_properties, :through => :managerships, :classname => 'Property'
end

class Property < ActiveRecord::Base
  has_many :tenants, :class_name => 'User', :foreign_key => 'residence_id'
  has_many :managerships
  has_many :ownerships
  has_many :owners, :class_name => 'User', :through => :ownerships
  has_many :managers, :class_name => 'User', :through => :managerships
end

Like I said, this is quick and dirty, a high-level first pass. Note that we've now created Managership and Ownership roles that can contain Manager and Owner-specific functionality, eliminating the former dilemma as to whether to silo that functionality in separate modules.

** Note also that I've inverted the Tenant/Property role as well -- I think this was a necessary change to your domain. Obviously a residence can have more than one tenant. It seems to me (at the moment) that you can keep the tenant-specific functionality on the User model.



回答2:

What does strike me is that your current model prohibits having multiple tenants or landlords at a given property, and assumes that these relationships are permanent.

In the UK at least, it's very common to have the situation where a property is owned by a married couple, and therefore could have multiple landlords (my wife and I are in this position, for example). Equally, for certain types of letting, it's very common for there to be multiple tenants at a single property.

If you think I'm right here, it's worth considering introducing a 'user_property_role' model object between the user and the property. You'd then have a has_many relationship from both user and property to user_property_role. If this role had a 'relationship type' field that you could set to e.g. 'landlord', you could then use has_many :through and named scopes (on the user_property_role) object to do e.g. property.users.landlords or property.users.tenants.

Taking this approach would also allow you do to things like give these relationships 'begin' and 'end' dates, recording the fact that a property has multiple tenants over time, for example, or that a property might be managed by different people over time. Again, you should be able to build this into a set of named scopes so that you can do e.g. property.users.current.tenants or even user.properties.current.tenants.

Hope that helps, and doesn't just add to your confusion!!