20220110のGoに関する記事は3件です。

【Go】APIサーバを構築する② ~データの作成(Create)/更新(Update)/削除(Delete)~

はじめに 前記事の続きでデータの作成、更新、削除の処理を追加しました。 実装 各処理の実装内容について記述します。 削除(Delete) まずはroutes.goにルートハンドラーを追加します。 routes.go router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie) 処理の内容をmovie-handlers.goに記述します。 パスパラメータを取得するとき、strconv.Atoi(params.ByName("id"))でidの型をStringからIntに変換することに注意します。 movie-handlers.go func (app *application) deleteMovie(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) // パスパラメータのidをIntに変換する id, err := strconv.Atoi(params.ByName("id")) if err != nil { app.errorJSON(w, err) return } // データの削除処理を行う err = app.models.DB.DeleteMovie(id) if err != nil { app.errorJSON(w, err) return } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } DBデータの削除処理DeleteMovie()はmovies-db.goに記述します。 データの取得処理にはQueryContext()を使用していましたが、データを変更する処理(削除、更新など)を行う場合にはexecContext()を使用します。 movies-db.go func (m *DBModel) DeleteMovie(id int) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := "delete from movies where id = $1" _, err := m.DB.ExecContext(ctx, stmt, id) if err != nil { return err } return nil } 作成(Create)/更新(Update) データの作成と更新については、既存のデータを編集するかどうかの違いだけで処理自体はほぼ同じです。 そのため、ルートハンドラーは一つにまとめてしまいます。 routes.go router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie) 削除のときと同様に、処理の中身はmovie-handler.goに記述します。 まずはリクエストデータの型をMoviePayloadという構造体で定義します。 payloadという構造体を定義し、json.NewDecoder(r.Body).Decode(&payload)でリクエストデータの中身r.Bodyをpayloadに入れます。 リクエストデータのレコードをDBに登録(作成)するために、movie.ID, _ = strconv.Atoi(payload.ID)のように、payloadのプロパティをmovieテーブルのカラムと同じ型に変換していきます。 そして、movie.ID == 0のときにレコード作成処理InsertMovie()、それ以外のときに更新処理UpdateMovie()を行うようにします。 movie-handler.go // JSONと同じ構造の構造体の型を定義する(入力値の型とDBのカラムの型は異なる) type MoviePayload struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Year string `json:"year"` ReleaseDate string `json:"release_date"` Runtime string `json:"runtime"` Rating string `json:"rating"` MPAARating string `json:"mpaa_rating"` } func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { // リクエストデータの型をもつ構造体を定義する var payload MoviePayload // JSONオブジェクトを読み込んでpayloadに代入する err := json.NewDecoder(r.Body).Decode(&payload) if err != nil { log.Println(err) app.errorJSON(w, err) return } // DBデータの型をもつ構造体を定義する var movie models.Movie // データ更新時にUpdatedAtを更新する if payload.ID != "0" { id, _ := strconv.Atoi(payload.ID) m, _ := app.models.DB.Get(id) movie = *m movie.UpdatedAt = time.Now() } // payloadの各プロパティの型を変換してmovieに代入する movie.ID, _ = strconv.Atoi(payload.ID) movie.Title = payload.Title movie.Description = payload.Description movie.ReleaseDate, _ = time.Parse("2006-01-02", payload.ReleaseDate) movie.Year = movie.ReleaseDate.Year() movie.Runtime, _ = strconv.Atoi(payload.Runtime) movie.Rating, _ = strconv.Atoi(payload.Rating) movie.MPAARating = payload.MPAARating movie.CreatedAt = time.Now() movie.UpdatedAt = time.Now() if movie.ID == 0 { // データ作成時の処理 err = app.models.DB.InsertMovie(movie) if err != nil { app.errorJSON(w, err) return } } else { // データ更新時の処理 err = app.models.DB.UpdateMovie(movie) if err != nil { app.errorJSON(w, err) return } } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } 各処理の中身は以下のようになります。 movie-db.go func (m *DBModel) InsertMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `insert into movies (title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.CreatedAt, movie.UpdatedAt, ) if err != nil { return err } return nil } movie-db.go func (m *DBModel) UpdateMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `update movies set title = $1, description = $2, year = $3, release_date = $4, runtime = $5, rating = $6, mpaa_rating = $7, updated_at = $8 where id = $9` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.UpdatedAt, movie.ID, ) if err != nil { return err } return nil } コード全文 以下をご覧ください。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】APIサーバの構築 ~データの作成(Create)/更新(Update)/削除(Delete)編~

はじめに 前記事の続きでデータの作成、更新、削除の処理を追加しました。 実装 各処理の実装内容について記述します。 削除(Delete) まずはroutes.goにルートハンドラーを追加します。 routes.go router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie) 処理の内容をmovie-handlers.goに記述します。 パスパラメータを取得するとき、strconv.Atoi(params.ByName("id"))でidの型をStringからIntに変換することに注意します。 movie-handlers.go func (app *application) deleteMovie(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) // パスパラメータのidをIntに変換する id, err := strconv.Atoi(params.ByName("id")) if err != nil { app.errorJSON(w, err) return } // データの削除処理を行う err = app.models.DB.DeleteMovie(id) if err != nil { app.errorJSON(w, err) return } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } DBデータの削除処理DeleteMovie()はmovies-db.goに記述します。 データの取得処理にはQueryContext()を使用していましたが、データを変更する処理(削除、更新など)を行う場合にはexecContext()を使用します。 movies-db.go func (m *DBModel) DeleteMovie(id int) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := "delete from movies where id = $1" _, err := m.DB.ExecContext(ctx, stmt, id) if err != nil { return err } return nil } 作成(Create)/更新(Update) データの作成と更新については、既存のデータを編集するかどうかの違いだけで処理自体はほぼ同じです。 そのため、ルートハンドラーは一つにまとめてしまいます。 routes.go router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie) 削除のときと同様に、処理の中身はmovie-handler.goに記述します。 まずはリクエストデータの型をMoviePayloadという構造体で定義します。 payloadという構造体を定義し、json.NewDecoder(r.Body).Decode(&payload)でリクエストデータの中身r.Bodyをpayloadに入れます。 リクエストデータのレコードをDBに登録(作成)するために、movie.ID, _ = strconv.Atoi(payload.ID)のように、payloadのプロパティをmovieテーブルのカラムと同じ型に変換していきます。 そして、movie.ID == 0のときにレコード作成処理InsertMovie()、それ以外のときに更新処理UpdateMovie()を行うようにします。 movie-handler.go // JSONと同じ構造の構造体の型を定義する(入力値の型とDBのカラムの型は異なる) type MoviePayload struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Year string `json:"year"` ReleaseDate string `json:"release_date"` Runtime string `json:"runtime"` Rating string `json:"rating"` MPAARating string `json:"mpaa_rating"` } func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { // リクエストデータの型をもつ構造体を定義する var payload MoviePayload // JSONオブジェクトを読み込んでpayloadに代入する err := json.NewDecoder(r.Body).Decode(&payload) if err != nil { log.Println(err) app.errorJSON(w, err) return } // DBデータの型をもつ構造体を定義する var movie models.Movie // データ更新時にUpdatedAtを更新する if payload.ID != "0" { id, _ := strconv.Atoi(payload.ID) m, _ := app.models.DB.Get(id) movie = *m movie.UpdatedAt = time.Now() } // payloadの各プロパティの型を変換してmovieに代入する movie.ID, _ = strconv.Atoi(payload.ID) movie.Title = payload.Title movie.Description = payload.Description movie.ReleaseDate, _ = time.Parse("2006-01-02", payload.ReleaseDate) movie.Year = movie.ReleaseDate.Year() movie.Runtime, _ = strconv.Atoi(payload.Runtime) movie.Rating, _ = strconv.Atoi(payload.Rating) movie.MPAARating = payload.MPAARating movie.CreatedAt = time.Now() movie.UpdatedAt = time.Now() if movie.ID == 0 { // データ作成時の処理 err = app.models.DB.InsertMovie(movie) if err != nil { app.errorJSON(w, err) return } } else { // データ更新時の処理 err = app.models.DB.UpdateMovie(movie) if err != nil { app.errorJSON(w, err) return } } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } 各処理の中身は以下のようになります。 movie-db.go func (m *DBModel) InsertMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `insert into movies (title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.CreatedAt, movie.UpdatedAt, ) if err != nil { return err } return nil } movie-db.go func (m *DBModel) UpdateMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `update movies set title = $1, description = $2, year = $3, release_date = $4, runtime = $5, rating = $6, mpaa_rating = $7, updated_at = $8 where id = $9` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.UpdatedAt, movie.ID, ) if err != nil { return err } return nil } コード全文 以下をご覧ください。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで動くフロントエンドが作れるSSRライブラリkyotoを使ってみた話

注意 本記事は試しに使ってみた程度の記事です。 今回使用したkyotoは開発初期であり、今後も多くの変更が予定されているようです。本記事はv0.2を使用しています。 kyotoとは kyotoはSSR(Server Side Rendering)のライブラリです。 ドキュメントにあるMotivationを以下に引用します。 Motivation Main motivation is to reduce usage of popular SPA/PWA frameworks where it's not needed because it adds a lot of complexity and overhead. There is no reason to bring significant runtime, VirtualDOM, and Webpack into the project with minimal dynamic frontend behavior. This project proves the possibility of keeping most of the logic on the server's side. 上記のようにkyotoはほとんどのロジックをサーバーサイドに置きながら、(制限はあるものの)SPA/PWAのようなフロントエンドを実現することができます。似たようなものとしてRailsのHotwireやLaravelのLaravel Livewireがあるようです。 Server Side Actions(SSA) kyotoではServer Side Actions(SSA)でフロントエンドの動作を実現できます。 SSAはブラウザとサーバー間で通信を維持した上で、ブラウザ側で起動したアクションをサーバー側で実行し、ブラウザはサーバから返ってきたHTMLを表示します。 現在SSAの通信はServer-sent events(SSE)で実装されているようですが、今後変更されるそうです(参考)。 今回作ったもの Redux Todos Exampleを参考にtodoリストを作りました。 使用したバージョンなど Go 1.17 https://github.com/kyoto-framework/kyoto 0.2 起動方法 $ git clone https://github.com/suger-131997/todos-kyoto.git $ cd todos-kyoto $ go mod download $ go run . http://localhost:9000/ サーバー起動 以下のコードでページのルーティングとポートを指定しています。同時にSSA用のhandlerも設定しています。 main.go func ssatemplate(p kyoto.Page) *template.Template { return template.Must(template.New("SSA").Funcs(kyoto.TFuncMap()).ParseGlob("*.html")) } func main() { mux := http.NewServeMux() // ルーティングの設定 mux.HandleFunc("/", kyoto.PageHandler(&PageTodos{})) // SSA用の設定 mux.HandleFunc("/ssa/", kyoto.SSAHandler(ssatemplate)) // サーバー起動 http.ListenAndServe("localhost:9000", mux) } ページ 各ページはhtmlのテンプレートと対応する構造体で作られます。 ページの構造体は使用するコンポーネントの構造体を要素として持ちます。 Init()はページを表示する際に初回に呼び出されて各種初期化を行います。 Template()内で使用するテンプレートを指定します。同時にコンポーネントのテンプレートも指定しています。 page.todos.go type PageTodos struct { Todos kyoto.Component } func (*PageTodos) Template() *template.Template { return template.Must(template.New("page.todos.html").Funcs(kyoto.TFuncMap()).ParseGlob("*.html")) } func (p *PageTodos) Init() { //コンポーネントの初期化 p.Todos = kyoto.RegC(p, &ComponentTodos{}) } {{ template {コンポーネント名} {コンポーネントの構造体} }}とする事でページ内にコンポーネントを埋め込むことができます。 SSAを使用する場合は{{ dynamics {SSA用のhandlerのURL} }}をhtml内に記述して、SSAのクライアント用のコードを埋め込む必要があります。 page.todos.html <html> <head> <title>kyoto todos</title> {{ dynamics `/ssa` }} </head> <body> {{ template "ComponentTodos" .Todos }} </body> </html> コンポーネント コンポーネントもhtmlのテンプレートと対応する構造体で作られます。 以下に示すのはフィルターを切り替えるボタンのためのコンポーネントです。 component.todos.filterlink.go type ComponentTodosFilterLink struct { Type FilterType CurrentFilter *FilterType } func (c ComponentTodosFilterLink) IsActive() bool { return c.Type == *c.CurrentFilter } コンポーネント部分を{{ define {コンポーネント名} }}と{{ end }}で囲むことでコンポーネントとして定義できます。 状態を持つコンポーネントの場合、top-level nodeに {{ componentattrs . }} を埋め込む必要があります。これによってhtml内にコンポーネントの情報が埋め込まれます。 component.todos.filterlink.html {{ define "ComponentTodosFilterLink" }} <button {{ componentattrs . }} onclick="{{ action `$ChangeFilter` .Type }}" {{ if .IsActive }} disabled {{ end }} >{{ .Type.String }}</button> {{ end }} 上記のようにonclick="{{ action {アクション名} {引数} }}"とする事でクリック時にSSAのアクションを呼び出すことができます。$がついているのは親コンポーネントのアクションを指定するためです(参考)。 コンポーネントを定義するためには以下に示すようにAction()をコンポーネントの構造体に定義します。 コンポーネントのアクションが呼び出された時にそのコンポーネントの再描画が行われます。 component.todos.go func (c *ComponentTodos) Actions() kyoto.ActionMap { return kyoto.ActionMap{ "Add": func(args ...interface{}) { c.Todos = append(c.Todos, Todo{c.NextTodoId, c.NewTitle, false}) c.NewTitle = "" c.NextTodoId++ }, "Complete": func(args ...interface{}) { target := int(args[0].(float64)) for i := 0; i < len(c.Todos); i++ { if c.Todos[i].Id == target { c.Todos[i].Completed = !c.Todos[i].Completed } } }, "ChangeFilter": func(args ...interface{}) { c.CurrentFilter = FilterType(int(args[0].(float64))) }, } } テキストボックスの入力取得 テキストボックスの入力をstringで受け取りたいときは、type="text"を指定した上で、下記のようにコンポーネントのメンバ変数をname, value, oninputに設定する必要がある。 component.todos.html <div> <input name="NewTitle" value="{{ .NewTitle }}" type="text" oninput="{{ bind `NewTitle` }}" /> <button onclick="{{ action `Add` }}" >Add Todo</button> </div> まとめ 動くフロントエンドが作れるSSRライブラリkyotoを使ってみました。 サーバーサイドでほとんどのロジックが動くおかげでかなり開発しやすいと感じました。基本的にGoとhtmlで完結できるという点もすごく魅力的です。 一方、公開するにはサーバーが必要となるため要件によっては、通常のフロントエンド用のライブラリを使用した方がいい場合も多そうです。(Github Pagesでの公開とかももちろんできない)。 開発も活発ですし、スターも結構ついているので今後追っていきたい気持ちです。 その他 類似してそうなライブラリ 記事を書いている途中に見つけた似てそうなものをまとめておきます。一切触ってないです。 よくわかってないこと あまりにも取り留めないので折り畳み SSAで渡されてくるint型がfloat64になっている クライアント側で状態を管理しているから? そもそもinterface{}で渡ってくるのどうにかなるといいな… コンポーネント間で共有する値の取り扱いがいまいちわからない 今回はポインタで共有してるけど正しいのかよくわからない。(状態はhtmlに埋め込まれてそうだけどポインタって?みたいな) Contextがあるからこれを使えってことなのだろうか? ContextでReduxみたいなものを共有する? Server Side Stateなるものが今後作られるらしいからそれかも 実際に外部サーバーに公開されたらどうなるんだろうか いまローカルで動いているレベルのサクサクさが期待できるのか Hotwireの方も似たような仕組みだし割と行けるのかも? たくさん接続されても大丈夫的なこともHotwireの方で言われていたし(参考) 再描画の制御がわかっていない アクションが起動されたコンポーネントとその子コンポーネントが書き換わるけど、それ以外の場合はどうしたらいいか分からない 使えてない機能&理解できていない機能がいくつもある Asyncとか Poll Triggerとか Control Rendering Modeとか 他複数
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む