前回までの記事で、ユーザ情報の登録画面と一覧リスト画面を実装しました。
ここまでくると、次は登録済データの編集機能も欲しくなりますね。
編集画面の実装を考える場合、入力項目は新規登録時と基本的に同じになりますので、それぞれ別にテンプレートを用意するのは煩雑に思えます。
できればテンプレート統一し、管理しやすくしたいところです。
そこで今回は、html/templateパッケージの分岐機能を使用し、同一のテンプレートを使用して新規登録画面と編集画面を実装する方法を解説します。
ベースとなる入力フォーム画面のテンプレート、および登録処理については以下の記事をご参照ください。
【Go実践】POSTされたデータを受け取って処理する ― 入力フォームと確認画面の作成
【Go実践】フォームで入力された情報をデータベースに登録する
また、各画面への遷移元として、前回の記事にて作成した一覧画面をトップページとします。
こちらの記事もあわせてご参照ください。
【Go実践】テンプレートでループを使用し一覧リストを作成する
Contents
テンプレートの分岐機能
テンプレートの分岐処理は以下のように記述できます。
条件に合致した場合の出力内容
{{ end }}
上記のように、条件に合致した場合のみ、{{ if }} ~ {{ end }}で囲った内容が出力されます。
{{ else }}や{{else if}}なども問題なく使用できます。
条件式の記述
一致を評価する演算子はeq、不一致を評価する演算子はneです。
また、演算子を使用して2項を比較する場合、比較対象する2項の前に記述する必要があります。
以下の例を参考にしてください。
{{ if eq .Id 1 }} // Idが1と一致する場合 {{ end }} {{ if ne .Id 1 }} // Idが1と一致しない場合 {{ end }} {{ if .Id }} // Idが存在する(nilでない)場合 {{ end }}
テンプレートで分岐を行う場合の留意点
テーブルやセレクトボックスのデータ行出力のためのループと異なり、条件によって出力内容を変えることは明確にビジネスロジックに該当します。
本来、デザインとロジックを分離することがテンプレートを使用する際の目的であり原則です。
したがって、テンプレート側で分岐処理を行うことは好ましい事とは言えません。
分岐機能を使用する場合、本当に必要かどうか設計を検討し、必要であっても最小限の利用にするよう心がけましょう。
入力フォーム画面のテンプレート修正
それでは、入力フォーム画面を新規登録と編集の両方に対応するように修正してみましょう。
templates/user-form.gtplを以下のように書き換えてください。
<!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="../asset/css/style.css"> </head> <body> {{if .Id}} <h2>ユーザ情報の編集</h2> <form action="user-update" method="post"> {{else}} <h2>ユーザ情報の登録</h2> <form action="user-confirm" method="post"> {{end}} <table> <tr> <td>アカウント名</td> <td><input type="text" name="account" value="{{.Account}}"></td> </tr> <tr> <td>お名前</td> <td><input type="text" name="name" value="{{.Name}}"></td> </tr> <tr> <td>パスワード</td> <td><input type="password" name="passwd" value="{{.Passwd}}"></td> </tr> </table> <input type="hidden" name="id" value="{{.Id}}"> <input type="submit" class="btn-push btn-push-blue" value="確認画面へ"> </form> </body> </html>
7行目から13行目が分岐処理になっています。
分岐の条件は、.Idがnilかどうかとしています。
.Idがnilでない場合は既存データの更新、.Idがnilである場合は新規登録です。
両条件でぞれぞれ、h2タグでのページ見出しと、POSTの送信先を分岐させています。
更新の場合はuser-updatel、新規登録の場合はuser-confirmlへsubmitさせています。
一覧画面のテンプレート修正
続いて、一覧画面のテンプレートを修正し、新規登録画面と変種画面へのリンクを追加します。
<!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="../asset/css/style.css"> </head> <body> <h2>ユーザ一覧</h2> <button type=“button” class="btn-push btn-push-blue" onclick="location.href='./user-form'">新規登録</button> <table class="list"> <tr> <th>ID</th> <th>アカウント名</th> <th>ユーザ名</th> <th>登録日時</th> <th></th> </tr> {{ range . }} <tr> <td>{{.Id}}</td> <td>{{.Account}}</td> <td>{{.Name}}</td> <td>{{.Created}}</td> <td><a href="./user-edit?id={{.Id}}">編集</a></td> </tr> {{ end }} </table> </form> </body> </html>
テーブルのデータ行に編集画面へのリンクを追加していますが、リンクにid={{.Id}}とすることで、リクエストパラメータで該当行のユーザIDを渡せるようにしています。
いったんここでmain.goを実行し、ブラウザでhttp://localhost:8080/user-listにアクセスしてください。
以下のように、テーブルの上に新規登録ボタンが、テーブル各行に編集カラムが出力されるはずです。
また、[新規登録]ボタンをクリックすると、意図通りに新規登録画面に遷移します。
分岐の一方、.Idフィールドが存在しない場合の判定は正常に動作しているといえます。
編集画面はまだ実装していないのでクリックしないでください。
編集画面の出力処理
続いて、編集画面/user-editへのリクエストハンドラとして使用する関数を実装します。
req_handlerディレクトリにh_UserEdit.goファイルを作成し、以下のように記述してください
package req_handler // 独自のHTTPリクエストハンドラパッケージ import ( "database/sql" "fmt" "html/template" "net/http" "strconv" "../conf" // 実装した設定パッケージの読み込み "../query" // 実装したクエリパッケージの読み込み // 実装したユーティリティパッケージの読み込み _ "github.com/go-sql-driver/mysql" ) // ユーザ情報編集画面 func HandlerUserEdit(w http.ResponseWriter, req *http.Request) { // テンプレートをパースする tpl := template.Must(template.ParseFiles("templates/user-form.gtpl")) // 設定ファイルを読み込む confDB, err := conf.ReadConfDB() if err != nil { fmt.Println("設定ファイルの読み込みに失敗しました。") } // 設定値から接続文字列を生成 conStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", confDB.User, confDB.Pass, confDB.Host, confDB.Port, confDB.DbName, confDB.Charset) // データベース接続 db, err := sql.Open("mysql", conStr) if err != nil { fmt.Println("データベース接続に失敗しました。") } // deferで処理終了前に必ず接続をクローズする defer db.Close() // リクエストパラメータからIDを取得してint64型に変換 reqid, _ := strconv.Atoi(req.FormValue("id")) trgid := int64(reqid) // ユーザマスタの単一行取得関数を実行 user, err := query.SelectUserById(trgid, db) // マップを展開してテンプレートを出力する if err := tpl.ExecuteTemplate(w, "user-form.gtpl", user); err != nil { fmt.Println(err) } }
フォームの初期値として登録済の情報を出力する必要があるため、リクエストURLのクエリパラメータからユーザIdを取得していユーザマスタへのSELECTを実行しています。
更新処理の実装
続いて、編集画面からの更新処理を記述します。
更新実行後は、新規登録時と同じくtemplates/user-registered.gtplを出力することとします。
package req_handler // 独自のHTTPリクエストハンドラパッケージ import ( "database/sql" "fmt" "html/template" "net/http" "../conf" // 実装した設定パッケージの読み込み "../query" // 実装したクエリパッケージの読み込み "../utility" // 実装したユーティリティパッケージの読み込み _ "github.com/go-sql-driver/mysql" ) // 更新結果の確認画面 func HandlerUserUpdate(w http.ResponseWriter, req *http.Request) { // POSTデータINSERT関数を実行 result := updatePostedUser(req) // テンプレートをパースする tpl := template.Must(template.ParseFiles("templates/user-registered.gtpl")) // テンプレートに出力する値をマップにセット values := map[string]string{ "result": result, } // マップを展開してテンプレートを出力する if err := tpl.ExecuteTemplate(w, "user-registered.gtpl", values); err != nil { fmt.Println(err) } } // POSTデータによる更新関数 func updatePostedUser(req *http.Request) string { // 正常終了時のreturn値 result := "ユーザ情報の更新に成功しました。" // 設定ファイルを読み込む confDB, err := conf.ReadConfDB() if err != nil { result = "設定ファイルの読み込みに失敗しました。" } // 設定値から接続文字列を生成 conStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", confDB.User, confDB.Pass, confDB.Host, confDB.Port, confDB.DbName, confDB.Charset) // データベース接続 db, err := sql.Open("mysql", conStr) if err != nil { result = "データベースへの接続に失敗しました。" } // deferで処理終了前に必ず接続をクローズする defer db.Close() // パスワードをハッシュ化(sha256) hashedPwd := utility.HashStr(req.FormValue("passwd"), "sha256") // POST値を渡してUPDATE処理を実行 err = query.UpdateUser(req.FormValue("account"), req.FormValue("name"), hashedPwd, req.FormValue("id"), db) if err != nil { result = "ユーザ情報の更新に失敗しました。" } // 結果をreturnする return result }
基本的には新規登録時と変わりありません。
POST値を使用してUPDATEを実行し、処理結果を流入してテンプレートを出力しています。
なお、ユーザマスタの更新に使用しているquery.UpdateUser関数の実装は以下の通りです。
// データ更新関数 func UpdateUser(acc, name, pw, id string, db *sql.DB) (err error) { // プリペアードステートメント stmt, err := db.Prepare("UPDATE M_USER SET ACCOUNT = ? ,NAME = ? , PASSWORD = ? WHERE ID = ?") if err != nil { return err } defer stmt.Close() // クエリ実行 _, err = stmt.Exec(acc, name, pw, id) if err != nil { return err } return nil }
処理結果画面のテンプレート修正
これは本題ではありませんが、処理結果画面から[戻る]ボタンをクリックした際の遷移先を一覧画面に変更しておきましょう。
<!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="../asset/css/style.css"> </head> <body> <h2>登録結果の確認</h2> <span>{{.result}}</span><br> <button type=“button” class="btn-push" onclick="location.href='./user-list'">一覧画面に戻る</button> </body> </html>
メイン処理にリクエストハンドラを追加する
最後に、編集画面のリンク/user-edite、および編集画面から更新実行した際の遷移先/user-update.へのHTTPリクエストハンドラをメイン処理に追加し、先に実装した関数へ対応させます。
package main import ( "net/http" "./req_handler" // 実装したHTTPリクエストハンドラパッケージの読み込み ) func main() { // "user-list"へのリクエストを関数で処理する http.HandleFunc("/user-list", req_handler.HandlerUserList) // "user-form"へのリクエストを関数で処理する http.HandleFunc("/user-form", req_handler.HandlerUserForm) // "user-confirm"へのリクエストを関数で処理する http.HandleFunc("/user-confirm", req_handler.HandlerUserConfirm) // "user-registered"へのリクエストを関数で処理する http.HandleFunc("/user-registered", req_handler.HandlerUserRegistered) // "user-edit"へのリクエストを関数で処理する http.HandleFunc("/user-edit", req_handler.HandlerUserEdit) // "user-update"へのリクエストを関数で処理する http.HandleFunc("/user-update", req_handler.HandlerUserUpdate) // css・js・イメージファイル等の静的ファイル格納パス http.Handle("/asset/", http.StripPrefix("/asset/", http.FileServer(http.Dir("asset/")))) // サーバーを起動 http.ListenAndServe(":8080", nil) }
リクエストハンドラを追記したら、再度main.goを実行し、ブラウザでhttp://localhost:8080/user-listにアクセスしてください。
先ほどと同じくユーザリスト画面が表示されます。
今度は編集機能の動作確認を行います。
ここでは、2行目の[編集]をクリックしてみてください。
下の画像のように、ページの見出しがユーザ情報の編集となっているはずです。
また、対象のユーザ情報がフォームの初期値にセットされています。
新規登録画面と編集画面の分岐は想定通りに動作していますね。
それでは、各項目の値を任意に書き換えて更新ボタンをクリックしてください。
以下のように処理が正常終了した旨が表示されると思いますので、ボタンをクリックして一覧画面に戻りましょう
一覧画面に戻ると、2行目のデータ内容が編集画面で入力した内容で更新されていることが確認できます。
更新処理も問題なく動作していますね。
終わりに
冒頭に書いた通り、テンプレートで条件分岐を行うことは基本的に好ましいことではありません。
しかし、似たような画面であれば、できる限りテンプレートを統一した方が管理しやすくなることも事実です。
ロジック側で出力内容を複雑に記述するより、テンプレートで分岐させた方がすっきりする場合も多いでしょう。
バランスを考えて、必要最小限の範囲で活用してください。