[詳解]golang+gormで画像をバイナリ化してDBに保存する

# はじめに
過去に長期インターンでRubyonRailsをやっていたのでその経験も活かして記事にしました。

## やりたいこと
今回は自分のプロフィール(冬仕様）であるこの画像をバイナリ化してDBに保存しようと思います。(184MB)

<img src="https://qiita-image-store.s3.amazonaws.com/0/153320/1532ad0d-86fc-9125-a5fa-8ff4f74de4c5.jpeg" width=30%>


この記事はRuby on Rails(RoR)を例にしてあげますが、全く理解できなくてもスルーしても読めるようにしたいとは思っています。

今回使用したVersionは以下の通りです。

- Golang 1.9.1
- gorm 0.6.3
- Ruby on Rails 5.0.3(参考用)

より良い記事にしたいので
誤字や間違いがありましたらコメントなどをいただけると幸いです。

# 方針
Ruby on Railsが、やすやすとここらへんを実装していて、自分も実装経験があるので、参考にしながら進めていきますが
**Railsはあくまでも比較のためにある**だけなので、さらっと読みたい人はGolang部分だけを見てください。

文法的なことはあまり解説しませんがコンタクトしてくださればわかる範囲で答えます！

# 実装
## DB
この記事では明示的に`ファイル`を示したいのでオブジェクトを`File`と名前をつけます。
命名法に違和感を抱く人がいたらすみません。

Railsでは以下のように書いて、ActiveRecord(Ruby製のORM)に働いてもらいます。

```Ruby:Migration_File.rb
class File < ActiveRecord::Migration
  def change
    create_table :files do |t|
      t.binary :upload_file
      t.timestamps
    end
  end
end
```

なんですが`upload_file`がRubyでいう`binary`でDBに書き込まれます。
正直、ORMは仲介人くらいなんでどうでもいいです。**要はDBにどんなデータ型で保存されるかが重要なんです。**

実際に見てみると（`create_at`など省略しています）


| Field       |      Type |   Null    |Key|Default|Extra             | 
|:-----------------:|:------------------:|:------------------:|:-----------------:|:------------------:|:------------------:|
| id             |              int(11) |        NO        |PRI|NULL|auto_increment|
| upload_file           | mediumblob |       YES       ||NULL||

となってました。
つまり`upload_file`の方は`mediumblob(8bit整数)`。
これ`[]byte`と同じ？？？！！！
だってgoでは`[]byte`と指定すると`VARBINARY`というデータ型になり、MySQLでは等価だからです。（※厳密には違う）
`[]byte`とは`[]uint8`のaliasです。詳しくは以下リンクで。
https://stackoverflow.com/questions/22950392/difference-between-uint8-byte-golang-slices

では、どのようにしたらbinary fileにできるのでしょうか？
ちょっとその前に、これを実装するために、データベースを作るためのdb.goを用意しておきます。

```go:db.go
package main

import (
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

var db *gorm.DB

type File struct {
	gorm.Model
	Source []byte
}

func init() {
	db, err = gorm.Open("mysql", "host:hogehoge@mysql/database")
	if err != nil {
		panic(err)
	}
	f = &File{}
	db.CreateTable(f)
	defer db.Close()
}
```

これを以下のコマンドで実行し、データベースを作成します。

```shell-session
$ go run db.go
```

データベース内容

| Field       |      Type |   Null    |Key|Default|Extra             | 
|:-----------------:|:------------------:|:------------------:|:-----------------:|:------------------:|:------------------:|
| id             |              int(11) |        NO        |PRI|NULL|auto_increment|
| upload_file           | VARBINARY |       YES       ||NULL||



## ImageのBinary化

###　最初に
JPEGを画像を扱うため`image/jpeg`がありますが、この他に標準ではGIF,PNGに対応しています。
この他の拡張子を使う場合は、他にパッケージを使うしかないようです。

Railsでは`<form>`によってアップロードされたファイルは`ActionDispatch::Http::UploadedFile クラス`なので`Read`メソッドで読んでDBに保存すればよかったのですがGolangではどのような感じでしょう。

ディレクトリ構造は以下のようになっています。

```
.
├── db.go　　　　　　　　　　　←　DB作成用
├── image.JPG 　←プロフ画
└── main.go   　←　実行フィアル
```

以下にコードを示します。
また、最終的なコードはこの記事の最後に書いています。

```go:main.go(一部)
	file, err := os.Open("image.JPG")
	if err != nil {
		log.Fatal(err)
	}

	img, err := jpeg.Decode(file)
	if err != nil {
		log.Fatal(err)
	}
	file.Close()

	buffer := new(bytes.Buffer)
	if err := jpeg.Encode(buffer, img, nil); err != nil {
		log.Println("unable to encode image.")
	}
	imageBytes := buffer.Bytes()

	f := &File{Source: imageBytes}
	db.Create(u)
```

## 解説
それではすこし解説をします。

まず開きたい画像ファイルのパスを`PATH`部分にいれて`os.Open(<PATH>)`で開きます。
問題なくいくと`*os.File`型の戻り値が返ってきます。

```go
	file, err := os.Open("image.JPG")
	if err != nil {
		log.Fatal(err)
	}
```

次に`image/jpeg`の`Decode`メソッドを使って`image.image`型の変数を作ります。
なぜこの`image.image`型を作るかというと、あとにエンコーディングをするときに必要だからです。

また、ここで注意したいのはかならず`file.Close()`をやってください。

```go
	img, err := jpeg.Decode(file)
	if err != nil {
		log.Fatal(err)
	}
	file.Close()
```

そしてファイルを生成してください。
同じく`file.Close()`をすることを忘れないでください。

そして`bytes`でバッファを扱うための便利なパッケージがあります。`bytes.Buffer`は書き込み用のインターフェイスを持っています。
`new`で`byte.Buffer`を初期化して、imgがもつ`image.image`型の変数をともに`jpeg.Encode`に渡しています。

```go
	buffer := new(bytes.Buffer)
	if err := jpeg.Encode(buffer, img, nil); err != nil {
		log.Println("unable to encode image.")
	}
	imageBytes := buffer.Bytes()
```

`json.Encode`をなぜこのようにするかというと[リファレンス]()を読んでみたらすぐわかります。


```go:image/jpegのリファレンス
func Encode(w io.Writer, m image.Image, o *Options) error
```
`byte.Buffer`は書き込み用のインターフェイス`io.Writer`を持っているのでわたすことができます。
そして、もとの`[]byte`型は`Bytes()`で取得できるので`imageBytes`に変換することができます。

```go
imageBytes := buffer.Bytes()
```

そしてこれを構造体に渡してDBに保存して終了です！

```go
f.Source = imageBytes
db.Create(f)
db.Close()
```

これでこの記事終了です！
**お疲れ様でした！！**

## 終わりじゃない、、、



というもの、まだ問題が残っています。
それはこのままでは動かないのです。

```
ERROR 1406 (22001): Data too long for column 'source' at row 1
````

実は今回の`binary`は長すぎるのです。
というもの、冒頭で、「`VARBINARY`と`MIDDLEBLOB`は等価だ！Rails通りだぜ！」みたいなことを書きました。

しかし、ここにきて痛い目に合うのです。
データの大きさが鍵となっています。

## 余談(Blob型の歴史)
もともとMySQLではバイナリ型はblob型で一括してまとめてました。（たしか5.0.7ぐらいまで）
しかしバージョンが上がると、`VARBINARY`や`VARCHAR`が出てきました。
MySQL公式ドキュメントから参照しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153320/6ca8ddfa-3695-6ad7-5ab9-af0eaa5beffb.png)

簡単な乗数の計算ですがだいたいblob型の大きさは

- TINYBLOB:255B
- BLOB:65,535B
- MIDDLEBLOB:16MB
- LONGBLOB:4GB

であり、`VARCHAR`や`VARBINARY`はというと`255B`です。
到底足りません。

## 解決する
話をもとに戻します。

データの`Size`に原因があることがわかりました。
冒頭のこの部分に注目してください。

![スクリーンショット 2017-11-19 5.23.45.png](https://qiita-image-store.s3.amazonaws.com/0/153320/a54f4cc6-22bd-a0f3-a8f3-84b05a946386.png)

**到底足りません！LONGBLOB型が必要です**。

Railsのときは指摘してなかったじゃないか！と思われるかもしれませんが、その通りで、勝手にデフォルトでなっていました。
Railsに親しい人は当然だと思いますが、実は`:limit => 3.gigabyte`として指定してすることで、今回の場合でいうと`LongBlob`に変更することができるのです。

```Ruby:Migration_File.rb
class File < ActiveRecord::Migration
  def change
    create_table :files do |t|
      t.binary :upload_file, :limit => 3.gigabyte
      t.timestamps
    end
  end
end
```

同様にgormにだってできます。
gormは構造体をマッピングするという特徴があるので、構造体をいじればどうにかなりそうですよね。
実際もそうで指定したいメンバに`gorm:"size:70000`などとしてあげると`LONGBlOB`に変わります。

```go
type File struct {
	gorm.Model
	Source []byte `gorm:"size:70000"`
}
```

このようにしてSizeを変更できたので最後に実行して終わりです。

```shell-session
$ go run main.go
```

DBを確認すると人間には優しくない文字列がありますが、それがバイナリです。
最後までお読みしてくださりありがとうございました。
お疲れ様でした。


## ソースコード

```go
package main

import (
	"bytes"
	"image/jpeg"
	"log"
	"os"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

type File struct {
	gorm.Model
	Source []byte `gorm:"size:70000"`
}

func main() {
	db, err := gorm.Open("mysql", "host:hogehoge@mysql/database")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	file, err := os.Open("image.JPG")
	if err != nil {
		log.Fatal(err)
	}

	img, err := jpeg.Decode(file)
	if err != nil {
		log.Fatal(err)
	}
	file.Close()

	buffer := new(bytes.Buffer)
	if err := jpeg.Encode(buffer, img, nil); err != nil {
		log.Println("unable to encode image.")
	}
	imageBytes := buffer.Bytes()

	ｆ := &File{Source: imageBytes}
	db.Create(ｆ)
	db.Close()
}
```

そして`run`すると実行できます。

```shell-session
$ go run main.go
```