はじめに
過去に長期インターンで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)に働いてもらいます。
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を用意しておきます。
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 ← 実行フィアル
以下にコードを示します。
また、最終的なコードはこの記事の最後に書いています。
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/jpeg
のDecode
メソッドを使ってimage.image
型の変数を作ります。
なぜこのimage.image
型を作るかというと、あとにエンコーディングをするときに必要だからです。
また、ここで注意したいのはかならずfile.Close()
をやってください。
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
に渡しています。
buffer := new(bytes.Buffer)
if err := jpeg.Encode(buffer, img, nil); err != nil {
log.Println("unable to encode image.")
}
imageBytes := buffer.Bytes()
json.Encode
をなぜこのようにするかというとリファレンスを読んでみたらすぐわかります。
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
は長すぎるのです。
というもの、冒頭で、「VARBINARY
とMIDDLEBLOB
は等価だ!Rails通りだぜ!」みたいなことを書きました。
しかし、ここにきて痛い目に合うのです。
データの大きさが鍵となっています。
余談(Blob型の歴史)
もともとMySQLではバイナリ型はblob型で一括してまとめてました。(たしか5.0.7ぐらいまで)
しかしバージョンが上がると、VARBINARY
やVARCHAR
が出てきました。
MySQL公式ドキュメントから参照しています。
簡単な乗数の計算ですがだいたいblob型の大きさは
- TINYBLOB:255B
- BLOB:65,535B
- MIDDLEBLOB:16MB
- LONGBLOB:4GB
であり、VARCHAR
やVARBINARY
はというと255B
です。
到底足りません。
解決する
話をもとに戻します。
データのSize
に原因があることがわかりました。
冒頭のこの部分に注目してください。
到底足りません!LONGBLOB型が必要です。
Railsのときは指摘してなかったじゃないか!と思われるかもしれませんが、その通りで、勝手にデフォルトでなっていました。
Railsに親しい人は当然だと思いますが、実は:limit => 3.gigabyte
として指定してすることで、今回の場合でいうとLongBlob
に変更することができるのです。
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()
f := &File{Source: imageBytes}
db.Create(f)
db.Close()
}
そしてrun
すると実行できます。
$ go run main.go