9

Tengo una aplicación de rieles que tiene tres tipos diferentes de usuarios y los necesito a todos para compartir la misma información de perfil común. Sin embargo, cada usuario diferente también tiene atributos únicos. No estoy seguro de cómo separar los diferentes campos.Registro activo de Rails: cómo modelar un escenario de usuario/perfil

  • de administración (sitio de ancho de administración)
  • propietario (de una tienda/etc)
  • miembro (como un miembro de una cooperativa)

estoy usando legado para autenticación y cancan para autorización. Por lo tanto, tengo un modelo de usuario con un conjunto de funciones que se pueden aplicar al usuario. Esta clase se ve esto:

class User < ActiveRecord::Base 
    # ... devise stuff omitted for brevity ... 

    # Roles association 
    has_many :assignments 
    has_many :roles, :through => :assignments 

    # For cancan: https://github.com/ryanb/cancan/wiki/Separate-Role-Model 
    def has_role?(role_sym) 
    roles.any? { |r| r.name.underscore.to_sym == role_sym } 
    end 
end 

Cada usuario tiene un perfil que incluye:

  • Primera & Apellido
  • Info dirección (ciudad/st/zip/etc)
  • Teléfono

No quiero contaminar el modelo de usuario con esta información, así que lo estoy lanzando en un modelo de perfil. Esta parte es bastante simple. Esto convierte el modelo de usuario en algo como esto:

class User < ActiveRecord::Base 
    # ... devise stuff omitted for brevity ... 
    # ... cancan stuff omitted for brevity ... 
    has_one :profile 
end 

Los campos adicionales es donde tengo algunos sentimientos de inquietud acerca de cómo modelar son ...

Si un usuario es un administrador, tendrán campos únicos, tales como:

  • admin_field_a: cadena
  • admin_field_b: cadena
  • etc

Si un usuario es un propietario que tendrán campos únicos ...

  • stripe_api_key: cadena
  • stripe_test_api_key: cadena
  • stripe_account_number: cadena
  • has_one: tienda # AR refence a otro modelo que el administrador y el miembro no tienen.

Si un usuario es miembro van a tener unos campos adicionales tales como:

  • stripe_account_number: cadena
  • belongs_to: tienda # la tienda que son un miembro de
  • has_many: nota

...

y un modelo tienda contendrá un has_many sobre los miembros por lo que trabajamos los miembros de la tienda.

El problema está en los campos adicionales. ¿Los configuro como clases diferentes? Ponerlos en una diferente He Actualmente intentado un par de modos distintos de configurar esto:

Una forma es configurar el modelo de usuario como root agregada

class User < ActiveRecord::Base 
    # ... 

    # Roles association 
    has_many :assignments 
    has_many :roles, :through => :assignments 

    # Profile and other object types 
    has_one :profile 
    has_one :admin 
    has_one :owner 
    has_one :member 

    # ... 
end 

La ventaja de este enfoque es la El modelo de usuario es la raíz y puede acceder a todo. La caída es que si el usuario es un "propietario", entonces las referencias de "administrador" y "miembro" serán nulas (y el cartesiano de las otras posibilidades: administrador pero no propietario o miembro, etc.).

La otra opción que estaba pensando era que cada tipo de heredar el usuario a partir del modelo del usuario como tal:

class User < ActiveRecord::Base 
    # ... other code removed for brevity 
    has_one :profile 
end 

class Admin < User 
    # admin fields 
end 

class Owner < User 
    # owner fields 
end 

class Member < User 
    # member fields 
end 

problema con esto es que estoy contaminando el objeto Usuario con todo tipo de de Nil en la tabla donde un tipo no necesita los valores de otro tipo/etc. Solo parece un poco desordenado, pero no estoy seguro.

La otra opción era crear cada tipo de cuenta como la raíz, pero tienen al usuario como un objeto secundario, como se muestra a continuación.

class Admin 
    has_one :user 
    # admin fields go here. 
end 

class Owner 
    has_one :user 
    # owner fields go here. 
end 

class Member 
    has_one :user 
    # member fields go here. 
end 

El problema con lo anterior es que no estoy seguro de cómo cargar la clase apropiada una vez el usuario inicie sesión. Voy a tener su user_id y voy a ser capaz de decir qué papel son (debido a la asociación de roles en el modelo de usuario), pero no estoy seguro de cómo pasar del usuario UP a un objeto raíz. ¿Métodos? ¿otro?

Conclusión tengo unas pocas maneras diferentes de hacer esto, pero no estoy seguro de lo que el enfoque de "rieles" correctas es. ¿Cuál es la forma correcta de modelar esto en rieles AR? (MySQL backend). Si no hay un enfoque "correcto", cuál es el mejor de los anteriores (también estoy abierto a otras ideas).

Gracias!

Respuesta

6

Mi respuesta asume que un usuario determinado solo puede ser un tipo de usuario, p. SOLO un administrador o SOLO un miembro. Si es así, este parece ser un trabajo perfecto para ActiveRecord's Polymorphic association.

class User < ActiveRecord::Base 
    # ... 

    belongs_to :privilege, :polymorphic => true 
end 

Esta asociación da al usuario un descriptor de acceso llamado 'privilegio' (a falta de un término mejor y para evitar la confusión de nombres que se harán evidentes más adelante). Debido a que es polimórfico, puede devolver una variedad de clases. La relación polimórfica requiere dos columnas en la tabla correspondiente: una (accessor)_type y (accessor)_id. En mi ejemplo, la tabla de Usuario ganaría dos campos: privilege_type y privilege_id que ActiveRecord combina para encontrar la entrada asociada durante las búsquedas.

Sus clases de administración, propietario y miembros tener este aspecto:

class Admin 
    has_one :user, :as => :privilege 
    # admin fields go here. 
end 

class Owner 
    has_one :user, :as => :privilege 
    # owner fields go here. 
end 

class Member 
    has_one :user, :as => :privilege 
    # member fields go here. 
end 

Ahora usted puede hacer cosas como esta:

u = User.new(:attribute1 => user_val1, ...) 
u.privilege = Admin.new(:admin_att1 => :admin_val1, ...) 
u.save! 
# Saves a new user (#3, for example) and a new 
# admin entry (#2 in my pretend world). 

u.privilege_type # Returns 'Admin' 
u.privilege_id # Returns 2 

u.privilege  # returns the Admin#2 instance. 
# ActiveRecord's SQL behind the scenes: 
# SELECT * FROM admin WHERE id=2 

u.privilege.is_a? Admin # returns true 
u.privilege.is_a? Member # returns false 

Admin.find(2).user  # returns User#3 
# ActiveRecord's SQL behind the scenes: 
# SELECT * FROM user WHERE privilege_type='Admin' 
# AND privilege_id=2 

recomendaría a tomar el campo (accessor)_type en la base de datos ENUM si espera que sea un conjunto conocido de valores. Un ENUM, en mi humilde opinión, es una mejor opción que un VARCHAR255 que Rails normalmente usaría de manera predeterminada, es más fácil/más rápido/más pequeño de indexar, pero hace más difíciles los cambios en el camino cuando tienes millones de usuarios. Además, el índice de la asociación correctamente:

add_column :privilege_type, "ENUM('Admin','Owner','Member')", :null => false 
add_column :privilege_id, :integer, :null => false 

add_index :user, [:privilege_type, :privilege_id], :unique => true 
add_index :user, :privilege_type 

El primer índice permite ActiveRecord para encontrar rápidamente la asociación inversa (por ejemplo, encontrar el usuario que tiene un privilegio de administración # 2) y el segundo índice permite buscar todos los administradores o todos los miembros.

Este RailsCast es un poco anticuado, pero un buen tutorial sobre las relaciones polimórficas, no obstante.

Una última nota: en su pregunta, indicó que Administrador, Propietario o Miembro era el tipo de usuario adecuado, pero como probablemente verá, tendría que explicar que su tabla de usuario tendría un campo user_type_type .

+0

Esta es la respuesta que yo Estoy marcando como "esto funcionó" porque cuando lo probé funcionó bastante bien. Gracias por la respuesta muy bien pensada y detallada de Jeffrey. :) Sin embargo, después de consultar con Michael Hartl (RailsTutorial.org) sobre algún MVP (mínimo producto viable), he decidido incluir todos los datos de perfil en el modelo de Usuario para simplificar las interacciones con el modelo. Si el MVP resulta exitoso y es rentable, entonces puedo arreglar esto más tarde. –

1

Soy probablemente no va a darle una sugerencia rieles aprobados, pero ..

La separación de su perfil es una buena decisión. Considere usar el Decorator pattern para el rol a. Puede tener un AdminUserDecorator, OwnerUserDecorator o MemberOwnerDecorator. También podría agregar dinámicamente los campos adicionales directamente en la instancia (es Ruby después de todo), pero creo que eso se pondría feo y complicado. (Si realmente desea hacer cosas malas, use un objeto de visitante para darle una instancia de su decorador a partir de un método en la clase de usuario.)

Además, ¿por qué poner la franja o la configuración de pago en el propietario en lugar de parte de la información de la tienda? A menos que tal vez un propietario puede tener múltiples tiendas y utilizar la misma información de pago para cada tienda.

ACTUALIZACIÓN: También debería sugerir el uso de TDD para eliminar lo que funciona.

+0

Sí, la configuración de pago está en el propietario porque el propietario puede tener muchas tiendas. –

1

Ya has aceptado una respuesta, pero por lo que sea, tengo una situación similar y elegí utilizar el enfoque "Modelo de usuario como raíz agregada". Mi modelo de usuario contiene toda la información del "perfil" y el usuario, para usar un ejemplo ficticio, tiene_una: comprador y tiene_una: vendedor. Utilizo un campo tinyint sencillo como bitflags para los roles que tiene el usuario, ya que los usuarios pueden ser tanto compradores como vendedores (o TBD otros roles que necesito en el futuro). Si se establece un bit, puede suponer que la asociación correspondiente no es nula (que no ha sido un problema para mí, ya que siempre compruebo los bitflags antes de usar la referencia de asociación). En realidad, no tengo demasiados campos únicos en mis modelos subordinados reales, pero es muy útil para mantener las cosas desordenadas cuando cada modelo subordinado tiene asociaciones adicionales, como si el vendedor tiene una: cuenta mercantil y un comprador tiene una: historial de compras, etc. Todavía no está activo, pero cuando lo haga, seguiré este post con cualquier problema que encuentre.

Cuestiones relacionadas