Link Search Menu Expand Document

9. 嵌套表單(1-to-1)

9-1 情境準備

Model 和 Model 之間的數據關聯,除了一對多(1-to-many)之外,還有一種是 一對一(1-to-1),一對一關聯算是一對多關聯的一種特例情況。

例如,一個常見的設計是讓 User 擁有 Profile,並且一個 User 用戶只會有一個 Profile 資料。

什麽時候會需要一對一的關聯設計呢?既然是一對一對應,那麽直接合並 profiles 的所有欄位在 users table 上,只用一個 User Model 也是可以啊。

會這樣設計的主要原因是為了節省資料庫查詢量,例如用戶的 Profile 儲存了用戶的詳細資料,一來很可能只有進到用戶詳細頁面(例如 users#show)才會用到、二來可能不是每一個用戶都有這個 Profile 細節資料,所以拆表之後可以讓絕大部分的操作都不需要去碰到 profiles table。對 User 來說尤其重要,因為登入之後,每一次的瀏覽,Rails 都會去資料庫撈出 User 資料設定 current_user

好,那讓我們來製作用戶 Profile:

執行 rails g model profile

編輯 db/migrate/201XXXXXXXX2545_create_profiles.rb

class CreateProfiles < ActiveRecord::Migration[5.0]
  def change
    create_table :profiles do |t|
      t.integer :user_id, :index => true
      t.string :legal_name
      t.date :birthday
      t.string :location
      t.string :education
      t.string :occupation
      t.text :bio
      t.text :specialty
      t.timestamps
    end
  end
end

執行 rake db:migrate

編輯 app/models/user.rb

  class User < ApplicationRecord
+    has_one :profile

編輯 app/models/profile.rb

  class Profile < ApplicationRecord
+   belongs_to :user
  end

接下來示範兩種表單 UI 的設計:

  • 方案一:單獨的 resource 編輯 UI
  • 方案二:嵌套(Nested)表單 UI

9-2 單獨的 resource 編輯 UI

針對 Profile 設計單獨的 CRUD,是之前就學過的設計,讓我們復習一下如何後台編輯 Profile:

編輯 config/routes.rb

   namespace :admin do
     root "events#index"
     resources :events
-    resources :users
+    resources :users do
+      resource :profile, :controller => "user_profiles"
+    end
  end

注意:這裡用單數 resource :profile,關於單數 resource 的說法請參考 4-2 根據用戶配置切換時區 一節。另外,因為預設的 controller 的命名是 profiles,這裡我們偏好自定義命名改為 user_prorfiles

編輯 app/views/admin/users/index.html.erb

  <td><%= link_to "Edit", edit_admin_user_path(user), :class => "btn btn-default" %></td>
+ <td><%= link_to "Edit Profile", edit_admin_user_profile_path(user), :class => "btn btn-default" %></td>

image

執行 rails g controller admin::user_profiles

編輯 app/controllers/admin/user_profiles_controller.rb

-  class Admin::UserProfilesController < ApplicationController
+  class Admin::UserProfilesController < AdminController
+
+    before_action :find_user_and_profile
+
+    def edit
+    end
+
+    def update
+      if @profile.update(profile_params)
+        redirect_to admin_users_path
+      else
+        render "edit"
+      end
+    end
+
+    protected
+
+    def find_user_and_profile
+      @user = User.find(params[:user_id])
+      # 因為新建的用戶並沒有 profile,所以這裡先檢查是否有 @user.profile,如果沒有的話就用 @user.create_profile 新建進資料庫
+      @profile = @user.profile || @user.create_profile
+    end
+
+    def profile_params
+      params.require(:profile).permit(:legal_name, :birthday, :location, :education, :occupation, :bio, :specialty)
+    end
+
+  end

新增 app/views/admin/user_profiles/edit.html.erb

<h2>Edit User Profile</h2>
<%= form_for @profile, :url => admin_user_profile_path(@user) do |f| %>

<div class="form-group">
  <%= f.label :legal_name %> <%= f.text_field :legal_name, :class =>
  "form-control" %>
</div>

<div class="form-group">
  <%= f.label :birthday %> <%= f.date_field :birthday, :class => "form-control"
  %>
</div>

<div class="form-group">
  <%= f.label :location %> <%= f.text_field :location, :class => "form-control"
  %>
</div>

<div class="form-group">
  <%= f.label :education %> <%= f.text_field :education, :class =>
  "form-control" %>
</div>

<div class="form-group">
  <%= f.label :occupation %> <%= f.text_field :occupation, :class =>
  "form-control" %>
</div>

<div class="form-group">
  <%= f.label :bio %> <%= f.text_area :bio, :class => "form-control" %>
</div>

<div class="form-group">
  <%= f.label :specialty %> <%= f.text_area :specialty, :class => "form-control"
  %>
</div>

<div class="form-group">
  <%= f.submit "Submit", :class => "btn btn-primary" %>
</div>

<% end %>

image

這樣就可以在後台編輯 User Profile 了。

9-3 嵌套(Nested)表單 UI

接下來的重頭戲是將編輯 User 和編輯 Profile 的表單合並在一起。很多時候這樣對用戶的操作會更方便,一次就可以編輯兩個 Model 的資料。接下來示範如何在前臺做這個 UI。

image

編輯 app/models/user.rb

+  accepts_nested_attributes_for :profile

編輯 app/controllers/users_controller.rb

   def edit
     @user = current_user
     # 跟剛才後台情況一樣,如果沒有 @user.profile,要先新建一個
     # unless @user.profile 等同於 if [email protected] 或 if @user.profile.nil?
+    @user.create_profile unless @user.profile
   end

   # (略)

   def user_params
-    params.require(:user).permit(:time_zone)
+    params.require(:user).permit(:time_zone, :profile_attributes => [:id, :legal_name, :birthday, :location, :education, :occupation, :bio, :specialty] )
   end

Rails 的 accepts_nested_attributes_for 作用是可以在更新 User 時,也順便可以更新 Profile 資料。

編輯 app/views/users/edit.html.erb 把 Profile 的表單加在本來的 User 表單裡面:

  <h2>Edit User</h2>

  <%= form_for @user do |f| %>

    <div class="form-group">
      <%= f.label :time_zone %>
      <%= f.time_zone_select :time_zone %>
    </div>

+   <%= f.fields_for :profile do |ff| %>
+     <div class="form-group">
+       <%= ff.label :legal_name %>
+       <%= ff.text_field :legal_name, :class => "form-control" %>
+     </div>
+
+     <div class="form-group">
+       <%= ff.label :birthday %>
+       <%= ff.date_field :birthday, :class => "form-control" %>
+     </div>
+
+     <div class="form-group">
+       <%= ff.label :location %>
+       <%= ff.text_field :location, :class => "form-control" %>
+     </div>
+
+     <div class="form-group">
+       <%= ff.label :education %>
+       <%= ff.text_field :education, :class => "form-control" %>
+     </div>
+
+     <div class="form-group">
+       <%= ff.label :occupation %>
+       <%= ff.text_field :occupation, :class => "form-control" %>
+     </div>
+
+     <div class="form-group">
+       <%= ff.label :bio %>
+       <%= ff.text_area :bio, :class => "form-control" %>
+     </div>
+
+     <div class="form-group">
+       <%= ff.label :specialty %>
+       <%= ff.text_area :specialty, :class => "form-control" %>
+     </div>
+   <% end %>

    <div class="form-group">
      <%= f.submit "Save", :class => "btn btn-primary" %>
    </div>

<% end %>

這樣就完成了,你可以送出編輯看看。

用這種嵌套(Nested)表單 UI 的話,我們不需要編輯 routes.rb 加上 profile 路由,也不需要 profiles_controller,因為透過 users_controller 就可以完成了。

image

9-4 Profile 的顯示

接著增加顯示頁面看看。

修改 app/controllers/users_controller.rb,新增 show action,順便重構一下都先調用 find_user 方法。

  class UsersController < ApplicationController

    before_action :authenticate_user!
+   before_action :find_user

+   def show
+   end

    def edit
-     @user = current_user
    end

   def update
-    @user = current_user
     if @user.update(user_params)
       flash[:notice] = "修改成功"
       redirect_to edit_user_path
     else
       render "edit"
     end
   end

   # (略)

   protected

+  def find_user
+    @user = current_user
+    @user.create_profile unless @user.profile
+  end

   # (略)

新增 app/views/users/show.html.erb

<h1>我的個人資料</h1>

<p>Email: <%= @user.email %></p>
<p>Timezone: <%= @user.time_zone %></p>
<p>Birthday: <%= @user.profile.birthday %></p>
<p>Bio: <%= simple_format @user.profile.bio %></p>

編輯 app/views/layout/application.html.erb 修改主選單加上這個連結,

+   <li><%= link_to('我的個人資料', user_path) %></li>
    <li><%= link_to('修改個人資料', edit_user_path) %></li>

image


Copyright © 2010-2022 Wen-Tien Chang All Rights Reserved.