Go
golang
ORM
GORM
Binary

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

More than 1 year has passed since last update.

はじめに

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

やりたいこと

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

この記事は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)に働いてもらいます。

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を用意しておきます。

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()
}

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

$ 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    ← 実行フィアル

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

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型の戻り値が返ってきます。

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

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

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

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

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

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

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

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

image/jpegのリファレンス
func Encode(w io.Writer, m image.Image, o *Options) error

byte.Bufferは書き込み用のインターフェイスio.Writerを持っているのでわたすことができます。
そして、もとの[]byte型はBytes()で取得できるのでimageBytesに変換することができます。

imageBytes := buffer.Bytes()

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

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

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

終わりじゃない、、、

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

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

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

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

余談(Blob型の歴史)

もともとMySQLではバイナリ型はblob型で一括してまとめてました。(たしか5.0.7ぐらいまで)
しかしバージョンが上がると、VARBINARYVARCHARが出てきました。
MySQL公式ドキュメントから参照しています。

image.png

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

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

であり、VARCHARVARBINARYはというと255Bです。
到底足りません。

解決する

話をもとに戻します。

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

スクリーンショット 2017-11-19 5.23.45.png

到底足りません!LONGBLOB型が必要です

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

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に変わります。

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

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

$ go run main.go

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

ソースコード

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すると実行できます。

$ go run main.go