以前使用 Rails 開發時,Rails 有一個非常方便的功能:Form 的欄位跟 ORM 是直接綁定的,因此你在 Controller 不需要寫任何抓 Form 欄位的 code 。
以一個簡單的論壇為例,完成文章的新增、更新、刪除只需要以下的 code 便可以完成。不管是 create
還是 update
method,你都會發現 post_params
這個東西,它其實就是 private 底下的 post_params
method ,用來過濾只接受哪些 Form 欄位。
class PostsController < ApplicationController
def create
@group = Group.find(params[:group_id])
@post = @group.posts.new(post_params)
@post.author = current_user
if @post.save
flash[:notice] = "Success!"
redirect_to edit_group_post_path(@group, @post)
else
render :new
end
end
def update
@post = current_user.posts.find(params[:id])
if @post.update_attributes(post_params)
respond_to do |format|
format.js
format.html { redirect_to edit_group_path(@post.group, @post) }
end
else
render :edit
end
end
def destroy
@post = current_user.posts.find(params[:id])
@post.destroy!
redirect_to manage_posts_path
end
private
def post_params
params.require(:post).permit(:title, :content, :group_id)
end
end
在 Golang 如果是用 net/http
package 的話,假設你要抓 post_name
這個欄位的值,你得這樣寫:
import (
"net/http"
)
func xxxHandler(w http.ResponseWriter, r *http.Request) {
post_name := r.FormValue("post_name")
}
由此可想而知,如果你有好幾個 handler 負責某個資源的新增、刪除、編輯、更新,而且每次都要抓許多 Form 欄位的話,你就只能重複寫同樣的 code ,好一點就是用一個 function 包起來,不過這樣依然無法真正解決問題。
後來為了解決這種 copy and paste 的窘境,我自己寫了一層封裝,實作類似 Rails 的機制,下文來談談我的解決思路。
首先,我使用的 ORM 是 Beego ORM ,因此下面的 code 都是針對此 ORM 設計的,不過待你理解思路以後,要套到自己使用的 ORM 上想必是很容易的。
假設我們有一個 model 叫 Post
,並且它有 Id
、 Title
、 Content
三個欄位,下面示範用 Beego ORM 如何新增、更新資料:
type Post struct {
Id int64 `orm:"pk;auto"`
Title string
Content string `orm:"type(text)"`
}
o := orm.NewOrm()
var post Post
post.Title = r.FormValue("post_title")
post.Content = r.FormValue("post_content")
_, err := o.Insert(&post)
if err != nil {
//do someting to handle error
}
post.Title = "updated version"
if _, err = o.Update(&post); err != nil {
//do something to handle error
}
現在只有兩個欄位可能覺得還好,但假設今天有十個欄位,code 就會變得非常亂,何不讓它自動抓 Form 的值更新呢?
首先讓我們看一下完成版的 API 。只要在 Model 的欄位加上 form 這個 struct tag ,而它的值則是指定其對應的 Form 欄位,在呼叫 Insert
以及 Update
兩個 function 的時候,第一個參數為 Model ,第二個參數則為 http request。
新增 Post:
type Post struct {
Id int64 `orm:"pk;auto"`
Title string `form:"post_title"`
Content string `orm:"type(text)" form:"post_content"`
}
id, err := Insert(&models.Post{}, r)
if err != nil {
//do something to handle error
}
更新 Post:
post := models.Post{Id: 1}
if err := Update(&post, r); err != nil {
//do something to handle error
}
完成這個功能的思路如下:
- 抓使用者在 Model 指定的 struct tag ,看 Form 裡面有沒有相應的欄位,如果有的話就抓出 Form 欄位的值。
- Beego ORM 不管是 Insert 還是 Update ,如果看 source code 的話就會發現 Model 參數的 type 是
interface{}
,因此我們只要透過reflect.ValueOf()
function 去抓 Model 的 value ,接著再呼叫 value 的interface()
method 就可以正確地作為呼叫 Beego ORM 的 function 時所需的參數。
完整的 code 如下:
import (
"github.com/astaxie/beego/orm"
"net/http"
"reflect"
)
func read(model interface{}) (reflect.Value, error) {
o := orm.NewOrm()
value := reflect.ValueOf(model)
err := o.Read(value.Interface())
return value, err
}
func Insert(model interface{}, req *http.Request) (int64, error) {
value := reflect.ValueOf(model)
fErr := fillInFormValue(value.Elem(), req)
if fErr != nil {
return 0, fErr
}
o := orm.NewOrm()
id, err := o.Insert(value.Interface())
return id, err
}
func fillInFormValue(model reflect.Value, req *http.Request) error {
for i := 0; i < model.NumField(); i++ {
field := model.Type().Field(i)
formFieldName := field.Tag.Get("form")
if formFieldName != "" {
if model.Field(i).Kind() == reflect.String {
model.Field(i).SetString(req.FormValue(formFieldName))
}
}
}
return nil
}
func Update(model interface{}, req *http.Request) error {
value, vErr := read(model)
if vErr != nil {
return vErr
}
fErr := fillInFormValue(value.Elem(), req)
if fErr != nil {
return fErr
}
o := orm.NewOrm()
_, updateErr := o.Update(value.Interface())
return updateErr
}
仔細看 fillInFormValue
這個 function 的話,你會發現我限制只更新 Model 裡面 type 為 string 的欄位,這部分你可以針對自己的需求新增對其他 type 的支援。另外,上面的 code 雖然只展示了如何封裝 Insert
以及 Update
這兩個 method 讓它自動抓 Form 欄位的值更新資料庫,如果你理解上面的 code 的話,相信要增加其他的功能不是問題。
如果你有更好的想法,也歡迎留言分享。