【问题标题】:How to structure a has_many association with a dynamic scope?如何构建具有动态范围的 has_many 关联?
【发布时间】:2023-04-10 04:04:01
【问题描述】:

我的数据库中有一个用户表。用户可以是“admin”或“manager”类型。

鉴于下面的模型和架​​构,我希望对于“经理”用户的每个实例,“管理员”用户可以选择经理所属的租户的一个、部分或全部位置,以便选择哪个经理可以控制的位置。

我的模型

class User < ActiveRecord::Base
  belongs_to :tenant
class Tenant < ActiveRecord::Base
  has_many :users, dependent: :destroy
  has_many :locations, dependent: :destroy
class Location < ActiveRecord::Base
  belongs_to :tenant, inverse_of: :locations

我尝试了两条路径

首先,尝试在 User 和 Location 模型之间建立一个作用域 has_many 关联。但是,我无法集中精力构建此范围,以便“管理员”用户可以选择“经理”用户可以控制的位置。

第二,在 users 表中设置一个受控位置属性。然后我设置了一些代码,以便“管理员”用户可以选择“经理”可以控制的位置,并填充其“受控位置”属性。但是,保存在数据库中(在受控位置数组中)的是字符串而不是位置实例。

这是我为第二条路径尝试的代码:

迁移

def change
  add_column :users, :controlled_locations, :string, array: true, default: []
end

在视图中

= f.input :controlled_locations, label: 'Select', collection: @tenant_locations, include_blank: "Anything", wrapper_html: { class: 'form-group' }, as: :check_boxes, include_hidden: false, input_html: {multiple: true}

在用户控制器中(在更新方法中)

if params["user"]["controlled_locations"]
  params["user"]["controlled_locations"].each do |l|
    resource.controlled_locations << Location.find(l.to_i)
  end
  resource.save!
end

我的期望

首先,我不太确定我尝试的第二条路径是一种好方法(将数组存储在数据库中)。因此,如果可能的话,我最好的选择是建立一个作用域关联。

如果第二条路径可行,我想得到的是这样的。假设登录管理员,我选择 ID 为 1 的用户(经理)可以控制一个位置(波士顿体育场):

user = User.find(1)
user.controlled_locations = [#<Location id: 55, name: "Boston Stadium", created_at: "2018-10-03 12:45:58", updated_at: "2018-10-03 12:45:58", tenant_id: 5>]

相反,我尝试后得到的是:

user = User.find(1)
user.controlled_locations = ["#<Location:0x007fd2be0717a8>"]

数组中保存的不是位置实例,而是纯字符串。

【问题讨论】:

  • 你说得对,在数据库中存储数组不是一个很好的解决方案。它不适用于 ActiveRecord 关联,并且是糟糕的数据库设计。一个更好的想法是在用户和位置之间实际设置一个连接表,这就是关系数据库的使用方式。
  • 你说得对,我可以简单地将用户和位置关联起来,将 'has_many :locations, through: :tenant' 添加到 User 模型中。但是,这会将所有位置与用户相关联,这不是我想要实现的目标。
  • 不,我的意思是创建一个连接模型,描述用户和位置之间的关系,并将其设置为多对多关联。这种授权通常通过角色系统完成 - 如果您需要示例,请查看 rolify gem。

标签: ruby-on-rails arrays ruby associations


【解决方案1】:

首先,您的代码缺少 Tenant 类中的 locations 关联。

class Tenant < ActiveRecord::Base
  has_many :users, dependent: :destroy
  has_many :locations

假设变量manager 有一个User 记录。那么它可以控制的位置是:

manager.tenant.locations

如果需要,您可以使用委托语句缩短它。

class User < ActiveRecord::Base
  belongs_to :tenant
  delegate :locations, to: :tenant

然后你可以调用它

manager.locations

【讨论】:

  • 感谢 Marlin,我忘记了租户模型中的“位置”关联。我已经相应地编辑了这个问题。您的解决方案会将某个租户的所有位置与用户相关联,这不是我想要实现的。我只想关联其中的一些。
【解决方案2】:

用于授权的常见模式是角色:

class User < ApplicationRecord
  has_many :user_roles
  has_many :roles, through: :user_roles

  def add_role(name, location)
    self.roles << Role.find_or_create_by(name: name, location: location)
  end

  def has_role?(name, location)
    self.roles.exists?(name: name, location: location)
  end
end

# rails g model role name:string
# make sure you add a unique index on name and location
class Role < ApplicationRecord
  belongs_to :location
  has_many :user_roles
  has_many :users, through: :user_roles
  validates_uniqueness_of :name, scope: :location_id
end

# rails g model user_role user:references role:references
# make sure you add a unique compound index on role_id and user_id
class UserRole < ApplicationRecord
  belongs_to :role
  belongs_to :user
  validates_uniqueness_of :user_id, scope: :role_id
end

class Location < ApplicationRecord
  has_many :roles
  has_many :users, through: :roles
end

通过使系统比受控位置关联更通用一点,您可以在不同情况下重复使用它。

假设登录管理员,我选择了 ID 为 1 的用户 (经理)可以控制一个位置(波士顿体育场)

User.find(1)
    .add_role(:manager, Location.find_by(name: "Boston Stadium"))

在实际的 MVC 术语中,您可以通过将角色设置为可以像任何其他资源一样进行 CRUD 的嵌套资源来做到这一点。可以使用accepts_nested_attributes 或 AJAX 在一个表单中编辑多个角色。

如果您想通过角色的存在来确定查询范围,请加入角色和用户角色表:

Location.joins(roles: :user_roles)
        .where(roles: { name: :manager })
        .where(user_roles: { user_id: 1 })

要对单个资源进行身份验证,您可以:

class ApplicationController < ActionController::Base
  protected 
  def deny_access
    redirect_to "your/sign_in/path", error: 'You are not authorized.'
  end
end

class LocationsController < ApplicationController
  # ...
  def update
    @location = Location.find(params[:location_id])
    deny_access and return unless current_user.has_role?(:manger, @location)
    # ...
  end
end

我会考虑使用rolifypundit,而不是滚动您自己的授权系统。

【讨论】:

    猜你喜欢
    • 2011-10-05
    • 2011-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-22
    相关资源
    最近更新 更多