【echo】Go言語でDB操作やファイル操作するミニマムAPI開発【Gorm】

# はじめに
運用中の検索システムに運用者用の機能拡張をしたいと思い、Go言語のフレームワークであるechoを使って簡単にAPIを開発してみました。

===2019.12.26 追記===
他にもgo/echoについて参考になる記事をリンクしておきます
[Go の echo ってWebサーバーでサクッと REST しちゃう](https://qiita.com/ezaki/items/62e806ae42828bb3567a)
[Go言語でEchoを用いて認証付きWebアプリの作成](https://qiita.com/x-color/items/24ff2491751f55e866cf)


# モチベーション
* 新しい言語の習得
最近、pythonしか書いていなかったため、技術の幅を広げるためにも新しい言語に触れたかった。

* 特殊環境へのアプリケーション持ち込み
インターネット接続がない環境へ持ち込みたいため、バイナリファイル化を使って環境を作らなくても本番環境で実行させたいと考えたため。(結局、コンテナ実装にしましたがw)

# アジェンダ
この記事での内容は以下です。

* 簡単なAPIの仕様説明
* echoを用いたAPI実装
 * GormによるDB操作
 * ファイル操作

# 簡単なAPIの仕様説明
今回はエクセルファイルを介したデータベースへの入出力的な操作を実装しました。
なんでGoでやるのかみたいなとこはありますが、気にしません。とにかくGoを書きたい。

## 仕様
処理1-1 データベースの情報を元にエクセルファイルを作成
処理1-2 作成したファイルを指定フォルダへ移動
処理2-1 必要情報が記入されたファイルを元にデータベースを操作

大きく1と2でエンドポイントを作りました。やることは単純なんですけど、Goだとちょっと面倒くさかったりします。

1でユーザーにファイルを受け取ってもらい、情報を記入したファイルを送ってもらい2で処理するイメージです。


# echoを用いたAPI実装
## ディレクトリ構成
以下のような構成にしました。シンプルにControllerとModelを用意しました。

ExtendFunction(ディレクトリ名)

* main.go(ルーティング&ロギングの設定)
* Controllers
 * createfile.go(処理1)
 * import.go(処理2) 
* Models
 * table.go
 * message.go
* Utilities
 * util.go
* DDL
 * sample.csv
 * full.csv
 
## echoのインストール
``go get``コマンドでechoパッケージをインストールするだけ。

```terminal
go get github.com/labstack/echo
```

## main.go(ルーティング&ロギングの設定)
main.goは実際の実行ファイルになります。
主にルーティングやロギングの設定を行なっています。

```main.go
package main

import (
  "./Controllers"

  "github.com/labstack/echo"
  "github.com/labstack/echo/middleware"
)

func main() {
    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/operation/extract", Controller.CreateFile())
    e.POST("/operation/import", Controller.ImportData())

    e.Start(":8010")
    // e.Start not definedと出力されたら、以下で実行を試みる
　　　　　　　// importに"github.com/labstack/echo/engine/standard"を追加
　　　　　　　// e.Run(standard.New(":8010")) 
}
```

まず、echoに関する2文とエンドポイント対応した関数が書いてあるgoファイルが置いてあるディレクトリであるControllersをimportします。

これでディレクトリ名.関数名で関数の呼び出しができます。(Controllers配下のpackageはControllersにします)

HTTPメソッド,エンドポイントURL,呼び出したい関数,リッスンするポートを指定することでアプリケーションサーバーとして起動できるようにします。

起動コマンドは以下になります。

```
go run main.go
```

## /operation/extract
まずは、ファイルに書き出す情報をデータベースから引き出します。データベース操作にはORMとしてGormを使います。

Gormに関してはこの記事を良かったら参考にしてください。
[【gorm】Go言語でORM触って見た](https://qiita.com/chan-p/items/cf3e007b82cc7fce2d81)

### Models
今回、データベースから引き出す情報はあるデータベースに登録してあるテーブル情報(テーブル名,テーブル和名,更新情報,説明のうちテーブル名と更新情報)にしました。

サンプルのテーブル情報が以下です。

![スクリーンショット 2018-12-16 23.45.57.png](https://qiita-image-store.s3.amazonaws.com/0/148317/03ed1b46-c3b6-fa49-8caa-0951d6037dfa.png)


この情報のテーブル和名にあたるtable_name_Jaと説明にあたるdescribeを埋めることを目的にします。

まず、このテーブル情報を扱うためのモデルを用意します。
カラム名に大文字を使うのはタブーなんですけどスルーしてください(笑)

```Models/table.go
package Models

type Table struct {
  Table_name string `json:table_name`
  Table_name_Ja string `gorm:"column:table_name_Ja"`
  Update_info string `json:update_info`
  Describe string `jsnon:describe`
}

func (m *Table) TableName() string {
  return "search_table"
}
```

ここで一つポイントなのが、定義した構造体の名前と実際のデータベースのテーブル名が異なる場合にTableName関数によって実際のデータベースのテーブル名をreturnしてあげると定義した名前と実際の名前が異なる場合でもGormで扱うことができます。

```Models/message.go
package Models

type Message struct {
  Mess string `json:"mess"`
}
```

下で説明しますが、メッセージ用のモデルも定義しておきます。

### Utilities/util.go
Gormでデータベースに接続する関数とメッセージをJsonで返す関数をutilitiesとして定義しておきます。

ReturnResponseはstringのメッセージ渡すとJsonで返せるのでデバッグのときなんかに使ってください。

ConnectPostgreSQLはデータベースへの接続関数になります。MySQLの場合は上記のGormのURLを参考にしてください。

```Utilities/util.go
package Utilities

import (
    "github.com/jinzhu/gorm"
    "encoding/json"
    _ "github.com/lib/pq"
    "os"
)

func ReturnResponse(mes string, c echo.Context) error {
  message := Models.Message{}
  json.Unmarshal([]byte(`{"Mess": "`+mes+`"}`), &message)
  return c.JSON(http.StatusOK, message)
}

func ConnectPostgreSQL() *gorm.DB {
    DBMS := os.Getenv("DBMS")
    USER := os.Getenv("USER")
    PASS := os.Getenv("PASSWORD")
    DBNAME := os.Getenv("NAME")
    HOST := os.Getenv("HOST")

    CONNECT := "user=" + USER + " password=" + PASS + " dbname=" + DBNAME + " host=" + HOST + " sslmode=disable"
    db, err := gorm.Open(DBMS, CONNECT)
    if err != nil {
        panic(err)
    }
    return db
}
```

ConnectPostgreSQLで、データベース情報をハードコードするのはよくないので環境変数を読み込んで設定するように書きました。

### Controllers/CreateFile.go
GetTabledataでテーブル名を渡したらテーブル情報をひっぱてきます。returnする型はModels.table型です。

WriteFileでひっぱてきたテーブル情報をcsvファイルに書き込みます。
まず./DDL/sample.csvという名前でファイルを生成します。osパッケージを使用します。
うまく生成できなかった場合にerrの値を使ったエラー処理を書いておきます。

csvパッケージ使って、書き込み用のインスタンスwriterを生成します。
write.Writeで書き込む内容をスタックして、write.flushで実際に書き込む行うイメージです。
うまく工程が進んだら、最後に成功メッセージを返しておきます(お好みで)

```Controllers/createfile.go
package Controllers

import (
  "os"
  "encoding/csv"

  "../Models"
  "../Utilities"

  "github.com/labstack/echo"
   _ "github.com/lib/pq"
)


func GetTabledata(tablename string) Models.Table {
  db := Utilities.ConnectPostgreSQL()
  defer db.Close()
  table := Models.Table{}
  db.First(&table, "table_name=?", tablename)
  return table
}

func WriteFile(table Models.Table) string {
  file, err := os.Create("./DDL/sample.csv")
  if err != nil {
    panic(err)
  }
  defer file.Close()
  file.Truncate(0)

  writer := csv.NewWriter(file)
  writer.Write([]string{"テーブル情報"})
  writer.Write([]string{"テーブル名", "テーブル和名", "更新情報", "説明"})
  writer.Write([]string{table.Table_name, "", table.Update_info, ""})
  writer.Flush()

  return "生成完了"
}

func CreateFile() echo.HandlerFunc {
  return func(c echo.Context) error {
    table := GetTabledata("sample_table")
    mes := WriteFile(table)
    return Utilities.ReturnResponse(mes, c)
  }
}

```

ここまでで、DDLのsample.csvにデータベースから引き出した情報の書き込みができました。

./DDL/sample.csv
![スクリーンショット 2018-12-17 1.25.38.png](https://qiita-image-store.s3.amazonaws.com/0/148317/ff4ed1ae-9370-f6d7-226b-a28df1934d48.png)


このファイルにユーザーが必要な情報(今回はテーブル和名,説明)を記載してもらい本API送ってもらうことで、そのファイル内容をデータベースに書き込みという作業を実装していきます。

## /operation/import
次に、ファイルを受け取り情報をデータベースに書き込みします。

### Controllers/import.go

```Controllers.import.go
package Controllers

import (
  "os"
  "encoding/csv"
  "io"

  "../Models"
  "../Connector"

  iconv "github.com/djimenez/iconv-go"
  "github.com/labstack/echo"
  _ "github.com/lib/pq"
)

func ImportData() echo.HandlerFunc {
  return func(c echo.Context) error {
    upload_file, err := c.FormFile("file")
    if err != nil {
        panic(err)
    }

    src, err := upload_file.Open()
    if err != nil {
        panic(err)
    }
    defer src.Close()

    dst_file, err := os.Create("./DDL/full.csv")
    if err != nil {
        panic(err)
    }
    defer dst_file.Close()

    if _, err = io.Copy(dst_file, src); err != nil {
        panic(err)
    }

    mess := InsertDB()
    return ReturnResponse(mess, c)
  }
}
```
c.FormFile('file')でPOSTリクエストで送られてきたファイルを受け取ります。
この受け取ったファイルをupdaload_file.Open()で開きます。

このファイル内容移すためのファイルを./DDL/full.csvとして生成しておきます。
ioパッケージのCopyを使って受け取ったファイル内容をfull.csvにコピーすることでローカルに保存します。
一つ一つの工程でエラー処理を挟んでおくと、どこでエラーがおきてるかわかってデバックしやすいかと(メッセージ関数とかも使って)

```Controllers/import.go
・・・・・
func InsertDB() string{
  db := Utilities.ConnectPostgreSQL()
  defer db.Close()

  fp, err := os.Open("./DDL/fullDLL.csv")
  if err != nil {
    panic(err)
  }
  defer fp.Close()

  // エンコード
  converter, err := iconv.NewReader(fp, "sjis", "utf-8")
  reader := csv.NewReader(converter)
  reader.Comma = ','
  reader.LazyQuotes = true

  count := 0
  for {
    record, err := reader.Read()
    if err == io.EOF {
      break
    } else if err != nil {
      return err.Error()
    }

    if (count == 0) || (count == 1) {
      count += 1
      continue
    }
    if count == 2 {
      newTable := Models.Table{record[0], record[1], record[2], record[3]}
      db.Create(&newTable)
    }
    count += 1
  }
  return "成功"
}
````

最後にデータベースに書き込む関数になります。
まず、データベースに接続します。今回はAPIが叩かれる頻度がそんなに多くなかったので都度、データベースに接続しています。
ローカルに保存してある受け取ったファイル内容がコピーされたファイルを開きます。

次に、ファイル内容をcsv.NewReaderに渡すのですが自分の場合エンコードが必要だったのでiconv-goパッケージでエンコードをはさんでいます。
iconv "github.com/djimenez/iconv-go"と書くことで他の言語でいうasのような扱いができます。今回のエンコードはsjis→utf-8にしました。

readerには何でスプリットするかを設定できるようで、今回はcsvファイルなので","にしています。
あとはreaderの内容をEOFが出るまでfor文でイテレーションしてファイル内容によってcontinueでとばしたり、gormでinsertをおこなっていきます。


## 実行
ここまできたら、```go run main.go``` で実行してlocalhost:8010/operation/extractにアクセスすると内部でsample.csvが生成されます。

この生成されたファイルに必要な情報を書き込み、ファイルをPOSTリクエストでlocalhost:8010/operation/importに送るとその内容がデータベースに書き込まれるようになっています。

./DDL/full.csv
![スクリーンショット 2018-12-17 1.26.00.png](https://qiita-image-store.s3.amazonaws.com/0/148317/310fed14-86b5-90e0-2528-770f54b9d47e.png)

こんな感じで記載して送ると、

![スクリーンショット 2018-12-17 1.24.40.png](https://qiita-image-store.s3.amazonaws.com/0/148317/1bcf744a-99ad-3030-aced-220092cc9bd9.png)

データベースに反映されます。
※ db.createをupdateに変更してもらう既存レコードの更新ができます(上記Gorm記事参照)

# 終わりに
echoを使ったファイル操作&データベース操作のAPIを書いてみました。

Goでこの用途のAPIを書く必要あるかと言われると、、、ですが、新しい言語に触れることができて楽しかったです。

Gormを使ったAPI例のような感じで参考にしていただければと思います。