Rails 4+:具有多个关联的单一资源,控制器组织
Rails 4+: single resource with several associations, controller organization
这是我正在努力寻找好的解决方案的 "real world project" 个问题之一。
我有一个 task
模型与几个不同的资源关联,我需要允许每个关联资源的 CRUD 功能。
例如,project
有很多 tasks
,我需要在 project
的上下文中更新 tasks
。
还有,每个project
有很多milestones
,每个milestone
也可以有很多tasks
。
在这种情况下,task
可能与 milestone
关联,也可能不关联。
class Project < ApplicationRecord
has_many :milestones
has_many :tasks
end
class Milestone < ApplicationRecord
belongs_to :project # required
has_many :tasks
end
class Task < ApplicationRecord
belongs_to :project # required
belongs_to :milestone # optional
end
我已经为我的控制器命名空间以便更好地组织和控制。这引导我走如下路线:
# routes.rb
resources :projects do
namespace :projects do
resources :milestones # app/controllers/projects/milestones_controller.rb
resources :tasks # app/controllers/projects/tasks_controller.rb
end
end
resources :milestones do
namespace :milestones do
resources :tasks # app/controllers/milestones/tasks_controller.rb
end
end
我也使用了很多 AJAX 来使界面更快(使用 Turbolinks),所以我的操作遵循 <action>.js.erb
格式,我使用 JavaScript 来更新页面而不是页面刷新。请注意,我也为 task
表单使用 dialog/popup 界面,因此我不能只刷新整个页面。
虽然这个 "works",但它最终导致我有很多重复代码的情况。
# projects/tasks_controller.rb
def new
@project = Project.find(params[:project_id])
@task = @project.tasks.new
end
def create
@project = Project.find(params[:project_id])
@task = @project.tasks.new(task_params)
if @task.save
# create.js.erb
else
render js: "alert('error');" # example...
end
end
# app/views/projects/tasks/create.js.erb
$("#tasks_for_<%= dom_id(@project) %>").append("<%=j render(partial: 'projects/tasks/task') %>");
# milestones/tasks_controller.rb
def new
@milestone = Milestone.find(params[:milestone_id])
@task = @milestone.tasks.new
end
def create
@milestone = Milestone.find(params[:milestone_id])
@task = @milestone.tasks.new(task_params)
if @task.save
# create.js.erb
else
render js: "alert('error');" # example...
end
end
# app/views/milestones/tasks/create.js.erb
$("#tasks_for_<%= dom_id(@milestone) %>").append("<%=j render(partial: 'milestones/tasks/task') %>");
这只是一些示例代码,来自实际系统的代码显示更多的重复。如您所知,每个不同资源中都有很多重复代码,它们与 tasks
资源的交互略有不同。
是否有一些标准格式或 Rails 功能可以帮助构建由其他几个资源操纵的资源?
我怎样才能减少这种重复?它直接导致了一个复杂的系统,每次我添加或更改一个功能,我都必须去 3+ 个不同的地方进行更改。
假设您的 ServiceBase
看起来像这样:
# services/service_base.rb
class ServiceBase
attr_accessor :args,
:controller
class << self
def call(args={})
new(args).call
end
end # Class Methods
#======================================================================
# Instance Methods
#======================================================================
def initialize(args)
@args = args
end
private
def params
controller.params
end
def assign_args
args.each do |k,v|
class_eval do
attr_accessor k
end
send("#{k}=",v)
end
end
end
然后 Tasks::NewService
看起来像这样:
# services/tasks/new_service.rb
class Tasks::NewService < ServiceBase
def call
assign_args
@haser = haser_klass.find(haser_id)
@haser.tasks.new
end
private
def haser_klass
haser_base.constantize
end
def haser_instance_name
haser_base.downcase
end
def haser_base
controller.class.name.split("::")[0].singularize
end
def haser_id
params["#{haser_instance_name}_id".to_sym]
end
end
然后,在您的控制器中,您应该能够执行如下操作:
# milestones/tasks_controller.rb
Milestones::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
end
# projects/tasks_controller.rb
Projects::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
end
如果您的 Tasks::CreateService
看起来像:
# services/tasks/create_service.rb
class Tasks::CreateService < ServiceBase
delegate :render,
to: :controller
def call
assign_args
@haser = haser_klass.find(haser_id)
@task = @haser.tasks.new(task_params)
if @task.save
# create.js.erb
else
render js: "alert('error');" # example...
end
end
private
def task_params
send("#{haser_instance_name}_params")
end
def milestone_params
params.require(:milestone).permit(:foo)
end
end
然后,在您的控制器中,您应该能够执行如下操作:
# milestones/tasks_controller.rb
Milestones::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
def create
Tasks::CreateService.call(controller: self)
end
end
# projects/tasks_controller.rb
Projects::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
def new
Tasks::CreateService.call(controller: self)
end
end
如果在抽象之后,您的控制器具有相同的方法,那么您可以开始做其他花哨的事情,比如让 Milestones::TasksController
和 Projects::TasksController
都继承自一个公共控制器除了具有所有共享方法的 ApplicationController
之外。因此,假设您没有使用 TasksController
。然后,也许您的控制器看起来像:
# tasks_controller.rb
TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
def create
Tasks::CreateService.call(controller: self)
end
end
# milestones/tasks_controller.rb
Milestones::TasksController < TasksController; end
# projects/tasks_controller.rb
Projects::TasksController < TasksController; end
现在,您只需在一个地方进行更改:在您的服务中。当然,如果您不想使用这些服务,您可以随时覆盖控制器方法。
这是我正在努力寻找好的解决方案的 "real world project" 个问题之一。
我有一个 task
模型与几个不同的资源关联,我需要允许每个关联资源的 CRUD 功能。
例如,project
有很多 tasks
,我需要在 project
的上下文中更新 tasks
。
还有,每个project
有很多milestones
,每个milestone
也可以有很多tasks
。
在这种情况下,task
可能与 milestone
关联,也可能不关联。
class Project < ApplicationRecord
has_many :milestones
has_many :tasks
end
class Milestone < ApplicationRecord
belongs_to :project # required
has_many :tasks
end
class Task < ApplicationRecord
belongs_to :project # required
belongs_to :milestone # optional
end
我已经为我的控制器命名空间以便更好地组织和控制。这引导我走如下路线:
# routes.rb
resources :projects do
namespace :projects do
resources :milestones # app/controllers/projects/milestones_controller.rb
resources :tasks # app/controllers/projects/tasks_controller.rb
end
end
resources :milestones do
namespace :milestones do
resources :tasks # app/controllers/milestones/tasks_controller.rb
end
end
我也使用了很多 AJAX 来使界面更快(使用 Turbolinks),所以我的操作遵循 <action>.js.erb
格式,我使用 JavaScript 来更新页面而不是页面刷新。请注意,我也为 task
表单使用 dialog/popup 界面,因此我不能只刷新整个页面。
虽然这个 "works",但它最终导致我有很多重复代码的情况。
# projects/tasks_controller.rb
def new
@project = Project.find(params[:project_id])
@task = @project.tasks.new
end
def create
@project = Project.find(params[:project_id])
@task = @project.tasks.new(task_params)
if @task.save
# create.js.erb
else
render js: "alert('error');" # example...
end
end
# app/views/projects/tasks/create.js.erb
$("#tasks_for_<%= dom_id(@project) %>").append("<%=j render(partial: 'projects/tasks/task') %>");
# milestones/tasks_controller.rb
def new
@milestone = Milestone.find(params[:milestone_id])
@task = @milestone.tasks.new
end
def create
@milestone = Milestone.find(params[:milestone_id])
@task = @milestone.tasks.new(task_params)
if @task.save
# create.js.erb
else
render js: "alert('error');" # example...
end
end
# app/views/milestones/tasks/create.js.erb
$("#tasks_for_<%= dom_id(@milestone) %>").append("<%=j render(partial: 'milestones/tasks/task') %>");
这只是一些示例代码,来自实际系统的代码显示更多的重复。如您所知,每个不同资源中都有很多重复代码,它们与 tasks
资源的交互略有不同。
是否有一些标准格式或 Rails 功能可以帮助构建由其他几个资源操纵的资源?
我怎样才能减少这种重复?它直接导致了一个复杂的系统,每次我添加或更改一个功能,我都必须去 3+ 个不同的地方进行更改。
假设您的 ServiceBase
看起来像这样:
# services/service_base.rb
class ServiceBase
attr_accessor :args,
:controller
class << self
def call(args={})
new(args).call
end
end # Class Methods
#======================================================================
# Instance Methods
#======================================================================
def initialize(args)
@args = args
end
private
def params
controller.params
end
def assign_args
args.each do |k,v|
class_eval do
attr_accessor k
end
send("#{k}=",v)
end
end
end
然后 Tasks::NewService
看起来像这样:
# services/tasks/new_service.rb
class Tasks::NewService < ServiceBase
def call
assign_args
@haser = haser_klass.find(haser_id)
@haser.tasks.new
end
private
def haser_klass
haser_base.constantize
end
def haser_instance_name
haser_base.downcase
end
def haser_base
controller.class.name.split("::")[0].singularize
end
def haser_id
params["#{haser_instance_name}_id".to_sym]
end
end
然后,在您的控制器中,您应该能够执行如下操作:
# milestones/tasks_controller.rb
Milestones::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
end
# projects/tasks_controller.rb
Projects::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
end
如果您的 Tasks::CreateService
看起来像:
# services/tasks/create_service.rb
class Tasks::CreateService < ServiceBase
delegate :render,
to: :controller
def call
assign_args
@haser = haser_klass.find(haser_id)
@task = @haser.tasks.new(task_params)
if @task.save
# create.js.erb
else
render js: "alert('error');" # example...
end
end
private
def task_params
send("#{haser_instance_name}_params")
end
def milestone_params
params.require(:milestone).permit(:foo)
end
end
然后,在您的控制器中,您应该能够执行如下操作:
# milestones/tasks_controller.rb
Milestones::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
def create
Tasks::CreateService.call(controller: self)
end
end
# projects/tasks_controller.rb
Projects::TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
def new
Tasks::CreateService.call(controller: self)
end
end
如果在抽象之后,您的控制器具有相同的方法,那么您可以开始做其他花哨的事情,比如让 Milestones::TasksController
和 Projects::TasksController
都继承自一个公共控制器除了具有所有共享方法的 ApplicationController
之外。因此,假设您没有使用 TasksController
。然后,也许您的控制器看起来像:
# tasks_controller.rb
TasksController < ApplicationController
def new
@task = Tasks::NewService.call(controller: self)
end
def create
Tasks::CreateService.call(controller: self)
end
end
# milestones/tasks_controller.rb
Milestones::TasksController < TasksController; end
# projects/tasks_controller.rb
Projects::TasksController < TasksController; end
现在,您只需在一个地方进行更改:在您的服务中。当然,如果您不想使用这些服务,您可以随时覆盖控制器方法。