20211123のGoに関する記事は5件です。

いろいろな言語でAPIコールするよ!

導入 webAPIの叩き方を各言語でまとめました。 後で見返す用です。 2021年11月23日時点:Postリクエスト送信 json リクエストのサンプルパラメータ sample_endpoint = "http://localhost:8888/sample" sample_parameter = { "title": "トマトスープ", "making_time": "15分", "serves": "5人", "ingredients": "玉ねぎ, トマト, スパイス, 水", "cost": "450" } Curl curl --location --request POST 'http://localhost:8888/recipes' \ --header 'Content-Type: application/json' \ --data-raw '{ "title": "トマトスープ", "making_time": "15分", "serves": "5人", "ingredients": "玉ねぎ, トマト, スパイス, 水", "cost": "450" }' Go package main import ( "fmt" "strings" "net/http" "io/ioutil" ) func main() { url := "http://localhost:8888/recipes" method := "POST" payload := strings.NewReader(`{ "title": "トマトスープ", "making_time": "15分", "serves": "5人", "ingredients": "玉ねぎ, トマト, スパイス, 水", "cost": "450" }`) client := &http.Client { } req, err := http.NewRequest(method, url, payload) if err != nil { fmt.Println(err) return } req.Header.Add("Content-Type", "application/json") res, err := client.Do(req) if err != nil { fmt.Println(err) return } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } php <?php require_once 'HTTP/Request2.php'; $request = new HTTP_Request2(); $request->setUrl('http://localhost:8888/recipes'); $request->setMethod(HTTP_Request2::METHOD_POST); $request->setConfig(array( 'follow_redirects' => TRUE )); $request->setHeader(array( 'Content-Type' => 'application/json' )); $request->setBody('{\n "title": "トマトスープ",\n "making_time": "15分",\n "serves": "5人",\n "ingredients": "玉ねぎ, トマト, スパイス, 水",\n "cost": "450"\n}'); try { $response = $request->send(); if ($response->getStatus() == 200) { echo $response->getBody(); } else { echo 'Unexpected HTTP status: ' . $response->getStatus() . ' ' . $response->getReasonPhrase(); } } catch(HTTP_Request2_Exception $e) { echo 'Error: ' . $e->getMessage(); } python import requests import json url = "http://localhost:8888/recipes" payload = json.dumps({ "title": "トマトスープ", "making_time": "15分", "serves": "5人", "ingredients": "玉ねぎ, トマト, スパイス, 水", "cost": "450" }) headers = { 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text) これから getパラメータの記述方法なども追記していく
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rclone】過去のコミットからプロジェクトの作り方やコーディングを学ぶ 1【Golang】

はじめに Golangを始めてから簡単なコーディングは理解できるようになったが、その先の学習方針に困った。 コーディングに関してはコードを読むのが一番だと思い、OSSの一つであるrcloneのコードを選んで読んでみることにする。 rcloneについて サードパーティ製クラウドデータ同期のコマンドラインツールである。特に公式のアプリの同期がうまくいかないときに導入を検討した人もいるのではないだろうか。幅広くクラウドサービスに対応しているようだ。 ちょうどGoogle Driveの同期が悪くて導入したかったので、中のコードを読んでみる Initial Commit Swiftssync 当初はSwitfとのファイルやディレクトリの同期を目的として作成されたようだ なので、Mainパッケージのファイル名もswiftssync.goとなっている。 コマンドラインのフラグを定義 コマンドラインからのフラグが定義されている。 swiftssync.go // Globals var ( // Flags //fileSize = flag.Int64("s", 1E9, "Size of the check files") cpuprofile = flag.String("cpuprofile", "", "Write cpu profile to file") //time.Hour & time.Minute = constants //duration = flag.Duration("duration", time.Hour*24, "Duration to run test") //statsInterval = flag.Duration("stats", time.Minute*1, "Interval to print stats") //logfile = flag.String("logfile", "stressdisk.log", "File to write log to set to empty to ignore") snet = flag.Bool("snet", false, "Use internal service network") // FIXME not implemented verbose = flag.Bool("verbose", false, "Print lots more stuff") quiet = flag.Bool("quiet", false, "Print as little stuff as possible") // FIXME make these part of swift so we get a standard set of flags? authUrl = flag.String("auth", os.Getenv("SWIFT_AUTH_USER"), "Auth URL for server. Defaults to environment var SWIFT_AUTH_USER.") userName = flag.String("user", os.Getenv("ST_USER"), "User name. Defaults to environment var ST_USER.") apiKey = flag.String("key", os.Getenv("ST_KEY"), "API key (password). Defaults to environment var ST_KEY.") ) コマンドラインのフラグとは コマンドラインで指定できるオプションのこと 例えば go build xxxx.go -v -o xxxx.exe における-v や -o のこと。 flagパッケージ Goではコマンドラインから受け取るフラグや引数の管理をflagパッケージで行うことができる。 authUrl = flag.String("auth", os.Getenv("SWIFT_AUTH_USER"), "Auth URL for server. Defaults to environment var SWIFT_AUTH_USER.") authUrl : フラグの変数のポインタ "auth" : フラグの名前、コマンドラインで指定するときの名前 os.Getenv("SWIFT_AUTH_USER") : フラグのデフォルト値 / ここでは環境変数からSWIFTのサーバーURLを取得している "Auth URL...." : このフラグに対する説明 / フラグの情報を見るフラグ -h or --help で見ることができる フラグについては以下のリンクがわかりやすい flag.Usage/PrintDefaults func main() { flag.Usage = syntaxError } func syntaxError() { fmt.Fprintf(os.Stderr, `Sync files and directores to and from swift FIXME Full options: `) flag.PrintDefaults() } flag.Usageのデフォルト定義は以下の通り var Usage = func() { fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n", os.Args[0]) PrintDefaults() } ここではsyntaxErrorという関数で書き直している 標準エラーが出たときに、それを出力してオプションのヘルプを表示する 試しにエラーが出るようなコードを叩いてみる .\swiftsync.exe -snet=4 snetはBooleanなので、当然エラーが出て invalid boolean value "4" for -snet: parse error Sync files and directores to and from swift FIXME Full options: -auth string Auth URL for server. Defaults to environment var SWIFT_AUTH_USER. -cpuprofile string Write cpu profile to file... となった。つまり、main()で必ずUsageを呼び出し、標準エラーがあったときはそのエラーとともにPrintDefaultsで使い方を表示するという仕様である。確かにexeファイルの中には実行した時にコマンドライン上でオプションを表示してくれるものを見かけたことがある。 flag.Usage = syntaxErrorの一文をコメントアウトして実行してみるとわかりやすい。 pprofによるプロファイリング プロファイリングとはコードのCPUやメモリのボトルネックを調査することで、プロファイルの方法としてよく耳にするのがベンチマークテストである。 Goでは主に3つの方法があるが、コード内で埋め込んで測定する方法としてはpprofパッケージが存在する initial commitではswiftへの接続処理のプロファイリングを行っている main() { // Setup profiling if desired if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } // swiftsの接続処理 // ...... } swift周りは検証しようがなかったので、とりあえず全てコメントアウトし、以下のコマンドを実行 .\swiftsync.exe -cpuprofile="test.prof" -user=testuser -key=testkey -auth=testu rl go tool pprof .\test.prof すると Type: cpu Time: Nov 23, 2021 at 6:27pm (JST) Duration: 216.06ms, Total samples = 0 No samples were found with the default sample value type. Try "sample_index" command to analyze different sample values. Entering interactive mode (type "help" for commands, "o" for options) (pprof) 実際には検証したい関数などの実行時間が見れる 使い方はわかったので、次に進む 参考 残りのコードについて nsToFloatStringやfloatStringToNsは秒⇔ナノ秒の変換を行う関数で、testコードもそれについてのものだった。 どう使うのかぱっと見で分からなかったので先のコミットを見ていくことにする。 総括 最初のコミットを見るだけで、コマンドラインのフラグや引数の管理、プロファイリングについて学ぶことができた。やっぱり知らないことを知るにはコード読むのが最適らしい。可能な限りコミットを追ってみたい。個人的にはGoogle Driveにつなぐときのコードを参考にしたいので、大きな改修がない限りは次のコミットはGoogle Driveの接続を書いてるところにしようかなと
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goのhttpパッケージの一部を理解する

初めに 参考になる記事がたくさんあるので、まずリンクを貼る 目的 自分はあまり理解力が高くないので、記事を見るだけでは理解できなかったりする。 参考になる記事たちを見て、改めて自分の中で整理してこの記事でアウトプットすることで自分の理解を深める 下記サンプルコードをもとに理解を深める main.go package main import ( "net/http" "github.com/koga456/sample-api/controller" "github.com/koga456/sample-api/model/repository" ) var tr = repository.NewTodoRepository() var tc = controller.NewTodoController(tr) var ro = controller.NewRouter(tc) func main() { server := http.Server{ Addr: ":8080", } http.HandleFunc("/todos/", ro.HandleTodosRequest) server.ListenAndServe() } コードリーディング server := http.Server{ Addr: ":8080", } server変数にhttpパッケージのServer型にAddrのみ指定して代入 http.HandleFunc("/todos/", ro.HandleTodosRequest) func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } pattern(パス)とfunc(ResponseWriter, *Request)型のhandlerを引数にDefaultServeMuxのHandleFuncメソッドを実行している // DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux DeualtServeMuxはServeMux型の変数。 要するにhttp.HandleFuncはServeMux型をレシーバにもつHandleFuncメソッドを実行している。 // HandleFunc registers the handler function for the given pattern func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) HandleFunc(httpパッケージのServeMux型をレシーバにもつHandleFuncメソッド) ServeMux型をレシーバに持つメソッドであり、pattern(/todos/)にhandler関数を登録(httpパッケージのServeMux型をレシーバに持つHandleメソッドを呼び出し)する serveMux型,muxEntry型 type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. hosts bool // whether any patterns contain hostnames } type muxEntry struct { h Handler pattern string } http.ServeMux.Handleは何をするのか ソースコード // Handle registers the handler for the given pattern. // If a handler already exists for pattern, Handle panics. func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() if pattern == "" { panic("http: invalid pattern") } if handler == nil { panic("http: nil handler") } if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } if mux.m == nil { mux.m = make(map[string]muxEntry) } e := muxEntry{h: handler, pattern: pattern} mux.m[pattern] = e if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } if pattern[0] != '/' { mux.hosts = true } } pattern(パス)に対応づけてmux[string]muxEntryとして登録している mux.Handle(pattern, HandlerFunc(handler)) handler(func ResponseWriter, *Request)をHandlerFunc型に変換してから渡すことで func (m *ServeMux) Handle(pattern string, handler Handler)のHandler型に納めている HandlerFunc型,Handler型 type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) type Handler interface { ServeHTTP(ResponseWriter, *Request) } HandlerFunc型はServeHttpメソッドを実装していて、 Handlerはインターフェースであり、ServeHttpを実装していることが求められていることがわかる 上記HandlerFunc型に変換しているのはServeHttpを実装させるため、実装していればHandler Interfaceに収まる。 server.ListenAndServe() 先ほど定義したserver変数のListenAndServeメソッドを実行している。 // ListenAndServe listens on the TCP network address srv.Addr and then calls Serve to handle requests on incoming connections. // Accepted connections are configured to enable TCP keep-alives. func (srv *Server) ListenAndServe() error server型 他にもたくさんあるけど、よく使う?ものだけ抜粋 type Server struct { // Addr optionally specifies the TCP address for the server to listen on, // in the form "host:port". If empty, ":http" (port 80) is used. // The service names are defined in RFC 6335 and assigned by IANA. // See net.Dial for details of the address format. Addr string Handler Handler // handler to invoke, http.DefaultServeMux if nil } 今回、server型のAddrのみ指定して、Handlerは指定しなかったため、http.DefaultServeMuxになる DefaultServeMuxについては上に書いたとおり、HandleFuncが呼び出されていく まとめ • 感想 今回色々な記事を見て頭に入らなかった原因がこの記事を書いている内にわかった 公式のdocを全然見てなかった   下記に理由はあるが、公式のドキュメントを読んでいなかった メソッドについての理解が足りていなかった   A Tour of Goをさらっと流し読みして、分かった気になっていたが、メソッド関数と関数の違いを分かっていなかったのでドキュメントを見てもイマイチピンと来なかった 後は書いている内に理解が深まっていった(わかっていないところがわかるようになった)ので、いい記事を書こうとは思わず、誰が見るわけでもないと思って気軽に書いていきたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gormで行うPagination(sort対応)

はじめに この記事では、Gormで行うPaginetionについて述べたいと思います。 また、paginationと同時にソートや検索等を行うことが多くあると思い、こちらに関しても記述させていただきました。 サンプルコードとして、Ginを使って行っていますが、echoでやる場合もほぼ同様にできます。 参考 Gormの公式pagination https://gorm.io/docs/scopes.html#Pagination 特に参考にした記事 https://dev.to/rafaelgfirmino/pagination-using-gorm-scopes-3k5f github githubにコードをまとめたので、分からない箇所等があれば見てみてください。 フォルダ名がpagenationになっているのは見てみぬふりをしてくれると助かります? 全体のAPI構成 今回、以下のような構成で行っています。 controllerやserviseに関しては今回の記事とは趣旨が異なるので説明は割愛させていただきます。 構成に関して以下の記事を参考にさせていただいています。 https://qiita.com/Asuforce/items/0bde8cabb30ac094fcb4 . ├── controller │   └── shop_controller.go #apiのエンドポイント ├── db │   └── db.go # dbのInitやClose ├── dto │   ├── page.go #pageのmodel │   └── shop_dto.go #レスポンスデータ(dto) ├── entity │   └── shop.go #entityオブジェクト ├── main.go ├── mapper │   └── page_mapper.go #pageobjへの変換 ├── query # gormのquery生成 │   ├── pagination.go │   └── sort.go ├── server │   └── server.go # routing等を記載 └── service └── shop_service.go # entityとdtoの変換 レスポンス 今回、レスポンスとしては以下のようなものを想定しています。 自分はpaginationの情報をpageでラッピングしていますが、_metadataのようにすると汎用性が高まるかもしれません。 https://stackoverflow.com/questions/12168624/pagination-response-payload-from-a-restful-api RequestUrl: http://localhost:8080/shops?size=2&&page=1&&direction=asc&&orderby=name { "page": { "number": 2, # page number "size": 2, #contents size "total_elements": 7, # total size "total_pages": 4 # total pages }, "shops": [ { "id": 7, "name": "Beauty-Beauty", "created_at": "2021-11-21T16:42:43.655756Z" }, { "id": 3, "name": "Ceauty-Beauty", "created_at": "2021-11-21T16:42:43.651779Z" } ] } 扱うエンティティ 今回扱うEntityです。shopsテーブルを例として扱います。 package entity import "time" type Shop struct { Id uint `json:"id"` Name string `json:"name"` CreatedAt time.Time `json:"created_at"` } モデルの作成 まず、pageのモデルを生成していきます。 package dto type Page struct { Number int `json:"number"` Size int `json:"size"` TotalElements int `json:"total_elements"` TotalPages int `json:"total_pages"` } pageの構造体には、現在の表示ページ(number)、1ページあたりに表示する件数(size)、件数(total_elements)、最後に全体のページ数(total_pages)を定義しています。 これだけの情報があれば、view上でpaginationを表現する際に必要な情報としては十分かと思います。 引用元: https://terasolunaorg.github.io/guideline/5.0.0.RELEASE/ja/ArchitectureInDetail/Pagination.html また、レスポンスとして返信するshopのdtoクラスも作成します。 レスポンスとしてはdbから受け取ったshopの配列及び、pageの情報を返します。 package dto import "github.com/DaisukeHirabayashi/golang-pagenation/entity" type ShopDto struct { Page Page `json:"page"` Shops []entity.Shop `json:"shops"` } mapperの作成 次に、mapperを生成します。 mapperではurlのqueryパラメーターと、データの大きさ(totalElements)を元にpageのオブジェクトを生成しています。 package mapper import ( "log" "math" "strconv" "github.com/DaisukeHirabayashi/golang-pagenation/dto" "github.com/gin-gonic/gin" ) func ConvertContextAndTotalElementsToPage(context *gin.Context, totalElements int) dto.Page { page, _ := strconv.Atoi(context.Query("page")) if page == 0 { page = 1 } pageSize, _ := strconv.Atoi(context.Query("size")) switch { case pageSize > totalElements: pageSize = totalElements case pageSize > 100: pageSize = 100 case pageSize <= 0: if totalElements < 5 { pageSize = totalElements } else { pageSize = 5 } } totalPages := int(math.Ceil(float64(totalElements) / float64(pageSize))) return dto.Page{Number: page, Size: pageSize, TotalElements: totalElements, TotalPages: totalPages} } 例として、下記のようなurlでリクエストを送信した場合、1ページあたりに表示する件数を2件、表示するpageを1としてpageオブジェクトを生成しています。 http://localhost:8080/shops?size=2&&page=1 また、queryパラメーターとしてsizeやpage情報を送信しなかった場合には初期値としてsize=5,page=1としています。 queryの作成 ここでは、pagenationの情報を基に、dbから必要な情報のみを取得するようにQueryを生成します。 package query import ( "github.com/DaisukeHirabayashi/golang-pagenation/dto" "github.com/jinzhu/gorm" ) type Pagination struct{} func (pagination Pagination) Pagination(page dto.Page) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { offset := (page.Number - 1) * page.Size return db.Offset(offset).Limit(page.Size) } } pageオブジェクトの内容を元にオフセットや個数を定義しQueryとして生成しています。 serviceの作成 package service import ( "github.com/DaisukeHirabayashi/golang-pagenation/db" "github.com/DaisukeHirabayashi/golang-pagenation/dto" "github.com/DaisukeHirabayashi/golang-pagenation/entity" "github.com/DaisukeHirabayashi/golang-pagenation/mapper" "github.com/DaisukeHirabayashi/golang-pagenation/query" "github.com/gin-gonic/gin" ) type ShopService struct{} var query_pagination query.Pagination func (shopService ShopService) GetShops(context *gin.Context) (dto.ShopDto, error) { db := db.GetDB() var shops []entity.Shop totalElements := db.Find(&shops).RowsAffected var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements)) if err := db.Scopes(query_pagination.Pagination(page)).Find(&shops).Error; err != nil { return dto.ShopDto{}, err } return dto.ShopDto{Page: page, Shops: shops}, nil } ここでは、ページネーションの情報に必要となる、データの総数を以下のようにして取得します。 totalElements := db.Find(&shops).RowsAffected その後、mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements))にて、totalElementsとurlのqueryパラメータを元にpageオブジェクトに変換します。 また、ScopeやFindによるQueryによってdbからデータを取得しています。 最後に、レスポンスとして必要となる情報をdto.ShopDto{Page: page, Shops: shops}にて返しています。 その他 検索 where句等で検索したものに対してpaginationを行うには、以下のようにすることで対処可能です。 今回のコードでは、全体の個数が特定されればpaginationができるようになっています。 したがって、検索で今回のコードを使いたい場合、serviceの箇所を以下のように変更することで可能となります。 totalElements := db.Where("title LIKE ? ","%"+context.Query("keyword")+"%")Find(&shops).RowsAffected var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements)) if err := db.Scopes(query_pagination.Pagination(page)).Where("title LIKE ? ","%"+context.Query("keyword")+"%").Find(&shops).Error; err != nil { return dto.ShopDto{}, err } } ソート ソートを行いたい場合、ソートのための関数をまず作成します。 package query import ( "strings" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" ) type Sort struct{} func (sort Sort) Sort(context *gin.Context) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { direction := context.Query("direction") if direction != "asc" && direction != "desc" { direction = "desc" } orderby := context.Query("orderby") if orderby == "" { orderby = "id" } order := strings.Join([]string{orderby, direction}, " ") return db.Order(order) } } ここでは、ソートの初期値をidの降順としています。 その後、検索の際と同様に以下のようにすることで対応可能です。 ソートの場合には全体の個数は変わらないのでtotalElementsの記述は変わりません。 totalElements := db.Find(&shops).RowsAffected var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements)) if err := db.Scopes(query_pagination.Pagination(page)).Scopes(query_sort.Sort(context)).Find(&shops).Error; err != nil { return dto.ShopDto{}, err } まとめ 今回、gormでのpaginationについてまとめてみました。 どのコードでも扱えるように共通化したつもりなのでよかったら参考にしてみてください。 以下、行ったことのまとめです。 データの総数をもとにpageのオブジェクト生成 pageのオブジェクトをもとに、dbからデータを取り出す。 pagenationを扱いたい場合、Scopes(query_pagination.Pagination(page))をメソッドチェーンでつなげる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Golang]Gormで行うPagination(sort対応)

はじめに この記事では、Gormで行うPaginetionについて述べたいと思います。 また、paginationと同時にソートや検索等を行うことが多くあると思い、こちらに関しても記述させていただきました。 サンプルコードとして、Ginを使って行っていますが、echoでやる場合もほぼ同様にできます。 参考 Gormの公式pagination https://gorm.io/docs/scopes.html#Pagination 特に参考にした記事 https://dev.to/rafaelgfirmino/pagination-using-gorm-scopes-3k5f github githubにコードをまとめたので、分からない箇所等があれば見てみてください。 フォルダ名がpagenationになっているのは見てみぬふりをしてくれると助かります? 全体のAPI構成 今回、以下のような構成で行っています。 controllerやserviseに関しては今回の記事とは趣旨が異なるので説明は割愛させていただきます。 構成に関して以下の記事を参考にさせていただいています。 https://qiita.com/Asuforce/items/0bde8cabb30ac094fcb4 . ├── controller │   └── shop_controller.go #apiのエンドポイント ├── db │   └── db.go # dbのInitやClose ├── dto │   ├── page.go #pageのmodel │   └── shop_dto.go #レスポンスデータ(dto) ├── entity │   └── shop.go #entityオブジェクト ├── main.go ├── mapper │   └── page_mapper.go #pageobjへの変換 ├── query # gormのquery生成 │   ├── pagination.go │   └── sort.go ├── server │   └── server.go # routing等を記載 └── service └── shop_service.go # entityとdtoの変換 レスポンス 今回、レスポンスとしては以下のようなものを想定しています。 自分はpaginationの情報をpageでラッピングしていますが、_metadataのようにすると汎用性が高まるかもしれません。 https://stackoverflow.com/questions/12168624/pagination-response-payload-from-a-restful-api RequestUrl: http://localhost:8080/shops?size=2&&page=1&&direction=asc&&orderby=name { "page": { "number": 2, # page number "size": 2, #contents size "total_elements": 7, # total size "total_pages": 4 # total pages }, "shops": [ { "id": 7, "name": "Beauty-Beauty", "created_at": "2021-11-21T16:42:43.655756Z" }, { "id": 3, "name": "Ceauty-Beauty", "created_at": "2021-11-21T16:42:43.651779Z" } ] } 扱うエンティティ 今回扱うEntityです。shopsテーブルを例として扱います。 package entity import "time" type Shop struct { Id uint `json:"id"` Name string `json:"name"` CreatedAt time.Time `json:"created_at"` } モデルの作成 まず、pageのモデルを生成していきます。 package dto type Page struct { Number int `json:"number"` Size int `json:"size"` TotalElements int `json:"total_elements"` TotalPages int `json:"total_pages"` } pageの構造体には、現在の表示ページ(number)、1ページあたりに表示する件数(size)、件数(total_elements)、最後に全体のページ数(total_pages)を定義しています。 これだけの情報があれば、view上でpaginationを表現する際に必要な情報としては十分かと思います。 引用元: https://terasolunaorg.github.io/guideline/5.0.0.RELEASE/ja/ArchitectureInDetail/Pagination.html また、レスポンスとして返信するshopのdtoクラスも作成します。 レスポンスとしてはdbから受け取ったshopの配列及び、pageの情報を返します。 package dto import "github.com/DaisukeHirabayashi/golang-pagenation/entity" type ShopDto struct { Page Page `json:"page"` Shops []entity.Shop `json:"shops"` } mapperの作成 次に、mapperを生成します。 mapperではurlのqueryパラメーターと、データの大きさ(totalElements)を元にpageのオブジェクトを生成しています。 package mapper import ( "log" "math" "strconv" "github.com/DaisukeHirabayashi/golang-pagenation/dto" "github.com/gin-gonic/gin" ) func ConvertContextAndTotalElementsToPage(context *gin.Context, totalElements int) dto.Page { page, _ := strconv.Atoi(context.Query("page")) if page == 0 { page = 1 } pageSize, _ := strconv.Atoi(context.Query("size")) switch { case pageSize > totalElements: pageSize = totalElements case pageSize > 100: pageSize = 100 case pageSize <= 0: if totalElements < 5 { pageSize = totalElements } else { pageSize = 5 } } totalPages := int(math.Ceil(float64(totalElements) / float64(pageSize))) return dto.Page{Number: page, Size: pageSize, TotalElements: totalElements, TotalPages: totalPages} } 例として、下記のようなurlでリクエストを送信した場合、1ページあたりに表示する件数を2件、表示するpageを1としてpageオブジェクトを生成しています。 http://localhost:8080/shops?size=2&&page=1 また、queryパラメーターとしてsizeやpage情報を送信しなかった場合には初期値としてsize=5,page=1としています。 queryの作成 ここでは、pagenationの情報を基に、dbから必要な情報のみを取得するようにQueryを生成します。 package query import ( "github.com/DaisukeHirabayashi/golang-pagenation/dto" "github.com/jinzhu/gorm" ) type Pagination struct{} func (pagination Pagination) Pagination(page dto.Page) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { offset := (page.Number - 1) * page.Size return db.Offset(offset).Limit(page.Size) } } pageオブジェクトの内容を元にオフセットや個数を定義しQueryとして生成しています。 serviceの作成 package service import ( "github.com/DaisukeHirabayashi/golang-pagenation/db" "github.com/DaisukeHirabayashi/golang-pagenation/dto" "github.com/DaisukeHirabayashi/golang-pagenation/entity" "github.com/DaisukeHirabayashi/golang-pagenation/mapper" "github.com/DaisukeHirabayashi/golang-pagenation/query" "github.com/gin-gonic/gin" ) type ShopService struct{} var query_pagination query.Pagination func (shopService ShopService) GetShops(context *gin.Context) (dto.ShopDto, error) { db := db.GetDB() var shops []entity.Shop totalElements := db.Find(&shops).RowsAffected var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements)) if err := db.Scopes(query_pagination.Pagination(page)).Find(&shops).Error; err != nil { return dto.ShopDto{}, err } return dto.ShopDto{Page: page, Shops: shops}, nil } ここでは、ページネーションの情報に必要となる、データの総数を以下のようにして取得します。 totalElements := db.Find(&shops).RowsAffected その後、mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements))にて、totalElementsとurlのqueryパラメータを元にpageオブジェクトに変換します。 また、ScopeやFindによるQueryによってdbからデータを取得しています。 最後に、レスポンスとして必要となる情報をdto.ShopDto{Page: page, Shops: shops}にて返しています。 その他 検索 where句等で検索したものに対してpaginationを行うには、以下のようにすることで対処可能です。 今回のコードでは、全体の個数が特定されればpaginationができるようになっています。 したがって、検索で今回のコードを使いたい場合、serviceの箇所を以下のように変更することで可能となります。 totalElements := db.Where("title LIKE ? ","%"+context.Query("keyword")+"%")Find(&shops).RowsAffected var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements)) if err := db.Scopes(query_pagination.Pagination(page)).Where("title LIKE ? ","%"+context.Query("keyword")+"%").Find(&shops).Error; err != nil { return dto.ShopDto{}, err } } ソート ソートを行いたい場合、ソートのための関数をまず作成します。 package query import ( "strings" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" ) type Sort struct{} func (sort Sort) Sort(context *gin.Context) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { direction := context.Query("direction") if direction != "asc" && direction != "desc" { direction = "desc" } orderby := context.Query("orderby") if orderby == "" { orderby = "id" } order := strings.Join([]string{orderby, direction}, " ") return db.Order(order) } } ここでは、ソートの初期値をidの降順としています。 その後、検索の際と同様に以下のようにすることで対応可能です。 ソートの場合には全体の個数は変わらないのでtotalElementsの記述は変わりません。 totalElements := db.Find(&shops).RowsAffected var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements)) if err := db.Scopes(query_pagination.Pagination(page)).Scopes(query_sort.Sort(context)).Find(&shops).Error; err != nil { return dto.ShopDto{}, err } まとめ 今回、gormでのpaginationについてまとめてみました。 どのコードでも扱えるように共通化したつもりなのでよかったら参考にしてみてください。 以下、行ったことのまとめです。 データの総数をもとにpageのオブジェクト生成 pageのオブジェクトをもとに、dbからデータを取り出す。 pagenationを扱いたい場合、Scopes(query_pagination.Pagination(page))をメソッドチェーンでつなげる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む