工作中遇到的一个问题。
背景
场景类似下面这样:
现有model project, model member, model user,可以将member看作是user与project之间多对多关系的中间表,现在需要在project新建后,将创建该project的当前用户current_user设为该项目的负责人,也就是在member中新增一条记录。同时在project 的new.html.erb
表单中,只有一个name,即项目名称字段。
如何实现?
我开始用了极为粗暴简单的方式,在projects_controller.rb
的create action中添加了这么一句:
def create
@project.save
Member.create!(role: "owner", project: @project, user: current_user)
respond_with @project, location: ok_url_or_default(Project)
end
这样功能是实现了,但是这明显不是正确的写法,找不到一点rails的影子,完全新手小白的方式。老大提到了用assign_attributes和accepts_nested_attributes_for来实现。改写了,实现了,踩了点坑。
记录下assign_attributes的用法,主要是与accepts_nested_attributes_for结合使用,在不依靠表单的情况下,更新数据库。
基本用法
assign_attributes,既可以更新model所有的attributes,也可以用来更新model的部分attributes。光看没什么感觉,拿个例子练练手。
有一个model movie,字段name, description, name要求不能为空,同时与user之间存在一对多的关联。
class Movie < ApplicationRecord
validates :name, presence: true
belongs_to :user
end
进入console试试:
m = Movie.new
### 只更新一部分
m.assign_attributes(name: "before you")
m.attributes
=> {"id"=>nil, "name"=>"before you", "description"=>nil, "created_at"=>nil, "updated_at"=>nil, "user_id"=>nil}
### 更新全部并保存
m.assign_attributes(name: "before you", description: "it is a love story", user: User.first)
m.save
=> true
m.attributes
=> {"id"=>7, "name"=>"before you", "description"=>"it is a love story", "created_at"=>Sat, 23 Jun 2018 01:25:15 UTC +00:00, "updated_at"=>Sat, 23 Jun 2018 01:25:15 UTC +00:00, "user_id"=>1}
上面的例子是create,如果要update呢?
m = Movie.first
=> #<Movie id: 4, name: "once", description: "Apart but still together", created_at: "2018-06-04 07:55:59", updated_at: "2018-06-04 07:55:59", user_id: 1>
m.assign_attributes(name: "one day")
m.attributes
=> {"id"=>4, "name"=>"one day", "description"=>"Apart but still together", "created_at"=>Mon, 04 Jun 2018 07:55:59 UTC +00:00, "updated_at"=>Mon, 04 Jun 2018 07:55:59 UTC +00:00, "user_id"=>1}
m.save
m
=> #<Movie id: 4, name: "one day", description: "Apart but still together", created_at: "2018-06-04 07:55:59", updated_at: "2018-06-23 01:30:41", user_id: 1>
可以看到成功完成了更新!
这么看,好像没什么,更新和新建而已,但是结合accepts_nested_attributes_for就很cool了。
下面的例子均以create为导向,不涉及update。
单个关联下
assign_attributes中,针对一对一的关联关系,有一个单独的private method,assign_nested_attributes_for_one_to_one_association, private method不能直接调用,用assign_attributes 和accepts_nested_attributes_for也可以实现一样的效果。
比如有一个model 别名alias , 与user之间是一对一的关系,每次创建alias时,需要关联到user。对应model中的代码如下:
Class Alias < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
这样使用assign_attributes,如果alias的user已经存在,则会更新,如果不存在,则新建一个对应的user。这里要注意,不要在user里面添加belongs_to :alias
, 这样你在传递user_attribute时,会报错,显示alias必须存在,而此时alias还没有创建成功。
进入console试试, 在alias未创建前,创建一个user:
a = Alias.new
a.assign_attributes({name: "ruby", user_attributes: { email: "[email protected]", password: "123456", password_confirmation: "123456"}})
a.save
=> true
a
=> #<Alias id: 1, name: "ruby", user_id: 3, created_at: "2018-06-23 02:33:23", updated_at: "2018-06-23 02:33:23">
a.user
=> #<User id: 3, email: "[email protected]", created_at: "2018-06-23 02:33:23", updated_at: "2018-06-23 02:33:23">
可以看到user已经创建完成!
不过我觉得这种情况,多半是你需要新建一个默认的user,因为参数的值是你传递过去的,已经定义好了,而不是像页面的表单那样。这个方法的好处在于,你不需要在页面表单中添加user部分,让用户选择,直接在创建alias时就与user建立起了关联。
多关联下
如同一对一关联中,一对多关系下,也有一个private method:assign_nested_attributes_for_collection_association。 官方给到的例子是这样的:
assign_nested_attributes_for_collection_association(:people, {
'1' => { id: '1', name: 'Peter' },
'2' => { name: 'John' },
'3' => { id: '2', _destroy: true }
})
People 是复数,这里传递的参数是一个hash,可以看出传了三条记录。如果用assign_attributes怎么实现呢?我这里直接用背景中那个问题来举例。【啧啧,在背景里卖关子没给到解答,悄悄把解答放在了这里。】
在project.rb
中,添加accepts_nested_attributes_for :members
:
class Project < ApplicationRecord
has_many :members, dependent: :destroy
accepts_nested_attributes_for :members
end
这样,我们可以修改controller中的action create了, 在save之前进行assign_attributes:
def create
@project.assign_attributes(members_attributes: [ { role: "owner", project: @project, user: current_user } ])
@project.save
respond_with @project, location: ok_url_or_default(Project)
end
踩坑的地方就在于members_attributes后面传递的是一个Array数组[ { role: "owner", project: @project, user: current_user } ]
,数组里面的元素才是Hash。启发来自于文档中的这个例子:
assign_nested_attributes_for_collection_association(:people, [
{ id: '1', name: 'Peter' },
{ name: 'John' },
{ id: '2', _destroy: true }
])
OK!
BTW,还有一个类似assign_attributes的method,update_attributes,官方没有给到例子,可以参考这篇文章accepts_nested_attributes_for is Creating New Records; Gotcha!,感觉跟assign_attributes还是很像的。
参考
assign_nested_attributes_for_one_to_one_association
assign_nested_attributes_for_collection_association
accepts_nested_attributes_for is Creating New Records; Gotcha!
一点思考
当我写出正确解答的时候,再看,发现其实挺简单的,可是我为什么花了差不多两个小时呢?因为我的搜索习惯。
曾经我的习惯是,出问题了就Google,看stack overflow,对于文档,觉得冗长枯燥,效率低,不愿意去读,其实这样反而是走了弯路。不懂的时候,看文档,看文档,看文档!不要偷懒去看别人写的那些解答和文章,极有可能完全不是你想要的,甚至可能让你离正确的解决方法越来越远。看文档虽然略显枯燥,却最有效,如果文档中没有相关的实例,可以去搜一下别人用例。
change your mind, change your habit.