Rails技术咨询:如何对用户、患者及医生角色进行建模?
Great question! Building a doctor-patient booking app like Practo/ZocDoc in Rails requires a solid user modeling strategy that balances simplicity, scalability, and separation of concerns. Let's break down the two most common approaches you can take, along with pros, cons, and actionable code examples.
1. Single Table Inheritance (STI) - The Simplest Approach
STI is perfect when your Patient and Doctor share most core attributes (like name, email, password) but have a few unique fields. It uses a single users table with a type column to distinguish between user roles.
Step-by-Step Implementation
1.1 Generate the Base User Model
Start with a User model that includes shared attributes plus fields unique to doctors (like approval status) and patients:
# Generate via Rails generator rails generate model User name:string email:string:uniq password_digest:string type:string approved:boolean:default=>false date_of_birth:date phone_number:string specialization:string education:text hospital:string rails db:migrate
1.2 Create Patient and Doctor Subclasses
These inherit from User and add role-specific validations or logic:
# app/models/user.rb class User < ApplicationRecord has_secure_password # Or use Devise for authentication # Shared validations validates :name, :email, presence: true end # app/models/patient.rb class Patient < User # Patient-specific validations validates :date_of_birth, :phone_number, presence: true # Associations: A patient has many appointments has_many :appointments, foreign_key: :patient_id, dependent: :destroy end # app/models/doctor.rb class Doctor < User # Doctor-specific validations validates :specialization, :education, :hospital, presence: true # Default scope to only show approved doctors to patients default_scope { where(approved: true) } # Associations: A doctor has many appointments has_many :appointments, foreign_key: :doctor_id, dependent: :destroy end
1.3 Handle Authentication & Authorization
Use Devise for authentication (recommended) and add role checks in controllers. For example, a doctor's dashboard controller:
# app/controllers/doctors/dashboard_controller.rb class Doctors::DashboardController < ApplicationController before_action :authenticate_user! before_action :ensure_doctor before_action :ensure_approved def index @upcoming_appointments = current_user.appointments.where('start_time > ?', Time.now) @patients = current_user.appointments.distinct.pluck(:patient) end private def ensure_doctor redirect_to root_path, alert: "Access denied." unless current_user.is_a?(Doctor) end def ensure_approved redirect_to root_path, alert: "Your account is pending admin approval." unless current_user.approved? end end
1.4 Route Setup
Use namespaces to separate patient and doctor routes:
# config/routes.rb Rails.application.routes.draw do devise_for :users namespace :doctors do get 'dashboard', to: 'dashboard#index' resources :appointments, only: [:index, :update] end namespace :patients do get 'dashboard', to: 'dashboard#index' resources :appointments, only: [:new, :create, :index] end root 'home#index' end
Pros & Cons of STI
- Pros: Minimal code, fast queries (no joins), easy to implement.
- Cons: The
userstable will have empty columns (e.g., patients don't needspecialization), which can get messy as roles grow.
2. Separate User + Associated Models - The Scalable Approach
If your Patient and Doctor have drastically different attributes (or you plan to add more roles like admins later), use a standalone User model for authentication, with separate Patient and Doctor models linked via one-to-one associations.
Step-by-Step Implementation
2.1 Generate Core Models
# User model (only authentication-related fields) rails generate model User name:string email:string:uniq password_digest:string rails db:migrate # Patient model (patient-specific fields) rails generate model Patient user:references date_of_birth:date phone_number:string rails db:migrate # Doctor model (doctor-specific fields + approval status) rails generate model Doctor user:references specialization:string education:text hospital:string approved:boolean:default=>false rails db:migrate
2.2 Define Associations
# app/models/user.rb class User < ApplicationRecord has_secure_password has_one :patient, dependent: :destroy has_one :doctor, dependent: :destroy # Helper methods to check role def patient? patient.present? end def doctor? doctor.present? end end # app/models/patient.rb class Patient < ApplicationRecord belongs_to :user validates :date_of_birth, :phone_number, presence: true has_many :appointments, dependent: :destroy end # app/models/doctor.rb class Doctor < ApplicationRecord belongs_to :user validates :specialization, :education, :hospital, presence: true default_scope { where(approved: true) } has_many :appointments, dependent: :destroy end
2.3 Handle Registration
When a user signs up, let them select their role and create the associated model:
# app/controllers/users/registrations_controller.rb (override Devise) class Users::RegistrationsController < Devise::RegistrationsController def create super do if params[:user][:role] == 'patient' resource.create_patient(patient_params) elsif params[:user][:role] == 'doctor' resource.create_doctor(doctor_params) end end end private def patient_params params.require(:patient).permit(:date_of_birth, :phone_number) end def doctor_params params.require(:doctor).permit(:specialization, :education, :hospital) end end
Pros & Cons of This Approach
- Pros: Clean, normalized database, easy to add new roles later, no empty columns.
- Cons: Requires more code, queries need joins (slight performance hit), and you'll have to manage two models per user during registration.
3. Key Considerations Beyond Modeling
- Approval Workflow: Add an admin interface to approve/reject doctor accounts. You can send email notifications using Action Mailer when a doctor's status changes.
- Authorization: Use gems like
PunditorCanCanCanto define granular permissions (e.g., doctors can only edit their own appointments, patients can't access doctor dashboards). - Appointment Model: Create an
Appointmentmodel withbelongs_to :patientandbelongs_to :doctor(orbelongs_to :user, polymorphic: trueif using STI) to link patients and doctors. - Dashboard Customization: Use partials and layout inheritance to keep patient/doctor dashboards distinct without repeating code.
4. Which Approach Should You Choose?
- Go with STI if your patient and doctor share most attributes and you want to get up and running quickly.
- Go with separate models if your roles have very different data needs, or you anticipate adding more roles (like clinic admins) in the future.
Either way, both patterns are well-supported in Rails and will work for your booking app use case!
内容的提问来源于stack exchange,提问作者Mohamed Ahmed

