Golang - 實作 ORM 與 Form 綁定,自動新增、更新資料庫內容

以前使用 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 ,並且它有 IdTitleContent 三個欄位,下面示範用 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	
}

完成這個功能的思路如下:

  1. 抓使用者在 Model 指定的 struct tag ,看 Form 裡面有沒有相應的欄位,如果有的話就抓出 Form 欄位的值。
  2. 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 的話,相信要增加其他的功能不是問題。

如果你有更好的想法,也歡迎留言分享。

comments powered by Disqus
分享至 Facebook 分享至 Google +