外部キーが設定されているとTRUNCATEできない #MariaDB #MySQL
inabaです。
最近はgolangでDBをいじっています。
テストもテスト用DBを作り、実際に書き込んで行うようにしていました。
テストをするたびにAUTO_INCREMENT
の値が増えていく
まぁそうでしょう。
なので、テストデータのクリアをするためにTRUNCATE
しようと思ってエラー出てハマりました。
エラー
2つのエラーが出ました。
プレースホルダ
_, err = db.Exec("TRUNCATE TABLE ?", tableName)
上記コードの時は下記のエラーが出ました。
Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '?' at line 1
テーブル名にプレースホルダは使えないらしい。
元々プレースホルダはキャッシュ目的だからFROM句は変えないですし、
セキュリティ的な使用でも、ユーザの入力をFROM句には使わないから妥当な動きですよね。
外部キー制約
プレースホルダをやめてfmt.Sprintfを使ってやるようにしてみたらエラー内容が変わりました。
_, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName))
下記がエラー。
Error 1701: Cannot truncate a table referenced in a foreign key constraint ...
これは、外部キー制約によってTRUNCATE
ができないというエラーでした。
外部キー制約の回避
外部キー制約を回避する方法があります。
_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 0") _, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName)) _, err = db.Exec("SET FOREIGN_KEY_CHECKS = 1")
上記コードではエラーは出ません。
FOREIGN_KEY_CHECKS
を0
に設定すると、外部キー制約の回避ができます。
別の解決法
AUTO_INCREMENT
が初期化されればよいのであれば、DELETEしてからAUTO_INCREMENT
をリセットすることもできます。
_, err = db.Exec(fmt.Sprintf("DELETE FROM %s", tableName)) _, err = db.Exec(fmt.Sprintf("ALTER TABLE %s AUTO_INCREMENT = 1", tableName))
これなら2行で済みます。
ただ、子テーブルにデータが入っているとエラーになるので、テストで子テーブルも使っている場合は子テーブルの方から消していってください。
今日はここまで。
【Go】structをJSONにする時、ゼロ値を含む/含まないを切り替える方法と注意点 #golang
inabaです。
Go言語でstructをJSONにする時、ゼロ値を含まない場合はタグにomitempty
をつけますよね。
type stampCard struct { Stamp int `json:"stamp,omitempty"` }
しかし、同じstructを使っている時にゼロ値を含む/含まないを切り替えたい時があります。
具体的には下記のようなstructです。
type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp int `json:"stamp,omitempty"` }
上記のstructではInStampRally
がtrueの時だけStamp
の値をJSONに含めたいです。
しかし、このstructのままだとInStampRally
がtrueでもStamp
が0なら含まれません。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp int `json:"stamp,omitempty"` } func main() { sc := stampCard{InStampRally: true, Stamp: 0} jsc, err := json.Marshal(pc) if err != nil { log.Error(err) } fmt.Println(string(jsc)) // {"in_stamp_rally":true} }
解決策
解決策はゼロ値を含む/含まないを切り替えたい値をポインタ化します。
type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` }
これで
という切り替えができます。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` } func main() { scs := []stampCard{ {InStampRally: false}, {InStampRally: true}, } for i, sc := range scs { if sc.InStampRally { scs[i].Stamp = intPointer(0) } } jscs, err := json.Marshal(scs) if err != nil { log.Error(err) } fmt.Println(string(jscs)) // [{"in_stamp_rally":false},{"in_stamp_rally":true,"stamp":0}] } func intPointer(v int) *int { return &v }
上記サンプルコードではstampCard
のスライスをループさせてInStampRally
がtrueなら値が0のintポインターをStamp
に代入しています。
こうすることで下記のようにomitempty
が指定されていてもゼロ値をJSONに出力することができます。
[ { "in_stamp_rally": false }, { "in_stamp_rally": true, "stamp": 0 } ]
注意点
rangeループのvalueからポインターを取ると、ループでvalueの値が変わってもポインターの値は同じなので、注意しないと意図しない値が入ってしまいます。
(ポインターの値が同じ理由は、ループのたびに新しいメモリを確保しないためでしょう。)
DBから取得した値をループさせてJSON化する時等、注意が必要です。
例として、下記は順番に0, 1, 2, 3
のスタンプ数のスタンプカードができる事を期待しているコードです。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` } func main() { stamps := []int{0, 1, 2, 3} var scs []stampCard for _, stamp := range stamps { log.Infof("%p", &stamp) sc := stampCard{InStampRally: true, Stamp: &stamp} scs = append(scs, sc) } jscs, err := json.Marshal(scs) if err != nil { log.Error(err) } fmt.Println(string(jscs)) }
しかし、上記の結果は下記になります。
$ go run main.go | jq INFO[0000] 0xc42000a3d0 INFO[0000] 0xc42000a3d0 INFO[0000] 0xc42000a3d0 INFO[0000] 0xc42000a3d0 [ { "in_stamp_rally": true, "stamp": 3 }, { "in_stamp_rally": true, "stamp": 3 }, { "in_stamp_rally": true, "stamp": 3 }, { "in_stamp_rally": true, "stamp": 3 } ]
まず、ループ中のvalueのポインター値を表示していますが、すべて同じになっています。
その後のJSONは全てのstamp
が3になってしまいました。
全ての値が最後の値に書き換わってしまっています。
修正後のコード
ループのvalueを一旦新しい変数に入れてその変数のポインターを取れば正確な値をJSONに出力できます。
package main import ( "encoding/json" "fmt" log "github.com/Sirupsen/logrus" ) type stampCard struct { InStampRally bool `json:"in_stamp_rally"` Stamp *int `json:"stamp,omitempty"` } func main() { stamps := []int{0, 1, 2, 3} var scs []stampCard for _, stamp := range stamps { tmp := stamp // important!! sc := stampCard{InStampRally: true, Stamp: &tmp} scs = append(scs, sc) } jscs, err := json.Marshal(scs) if err != nil { log.Error(err) } fmt.Println(string(jscs)) }
結果:
[ { "in_stamp_rally": true, "stamp": 0 }, { "in_stamp_rally": true, "stamp": 1 }, { "in_stamp_rally": true, "stamp": 2 }, { "in_stamp_rally": true, "stamp": 3 } ]
今日はここまで。
npm loginは空いているユーザ名を指定するとアカウントを作成する
inabaです。
npm owner add
で自分のパッケージにオーナーを追加できると知り、
試すためにログインが必要だったのでnpm login
でログインした時の話。
$ npm login Username: foobar Password: Email: (this IS public) foobar@example.com
上記を見てもらうとわかりますが、ユーザ名、パスワードの次にEmailの入力があります。
「なんでユーザ名入力してるのにEmailの入力も必要なのかな〜」という疑問が湧いてきたので調べてみた。
実はnpm login
は取得されていないユーザ名を指定すると新しいアカウントを作る挙動になっていた。
loginという名前だったのでこの挙動にはちょっと驚いた。
loginはnpm adduser
のエイリアスなんだそう。
上記issueではloginとadduserは別々のコマンドに分割する方向で考えているというような事が書いてあった。
今日はここまで。
git mergeのとき、デフォルトでno-ffになるようにする設定
inabaです。
ブランチのマージ時にマージ対象のブランチでどんな変更があったか知るためにマージコミットが欲しいので--no-ff
(No Fast-forward)オプションをつけていました。
$ git merge --no-ff foobar
ですが、設定でデフォルトをno-ffにできることを知りました。
設定
$ git config --global merge.ff false
上記設定をすることでgit merge foobar
のみでno-ffのマージになります。
$ git merge foobar ... $ git log --graph --oneline * f84d525 Merge branch 'foobar' |\ | * 2d68cba Fix B. |/ * a3159f3 Fix A.
参考サイト: Git - git-merge Documentation
問題
しかし、上記のみだとpull時もno-ffでマージコミットができてしまいます。
$ git pull ... $ git log --graph --oneline * 73018f7 Merge branch 'master' of github.com:178inaba/test |\ | * f84d525 Merge branch 'foobar' | |\ |/ / | * 2d68cba Fix B. |/ * a3159f3 Fix A.
よく言われることですがpullというのはfetchとリモートブランチのmergeの組み合わせなのでリモートブランチをマージする時に上記no-ffの設定が適用されてしまいます。
これを防ぐためにpull時にはFast-forwardでマージするように設定します。
$ git config --global pull.ff only
$ git log --graph --oneline * c7356cb Fix C. $ git pull ... $ git log --graph --oneline * bdbb39c Fix D. * c7356cb Fix C.
pullでマージコミットが作られないようになりました。
まとめ
$ git config --global merge.ff false $ git config --global pull.ff only $ cat ~/.gitconfig [merge] ff = false [pull] ff = only
今日はここまで。
HomebrewでインストールしたMariaDBをStrictモードに設定
inabaです。
MariaDBをHomebrewでインストールして使っていたのですが、
NOT NULL制約のカラムにDEFAULT NULLが設定されていると、INSERT時に値を設定していないカラムにNULLが入ってしまうという事がありました。
これはStrictモードが設定されていると起きないそうなので、Strictモードの設定をします。
デフォルトのsql_mode
Strictモードはmysqlにログイン後、sql_modeの値で設定されているか確認できます。
MariaDB [(none)]> SELECT @@SQL_MODE, @@GLOBAL.SQL_MODE; +--------------------------------------------+--------------------------------------------+ | @@SQL_MODE | @@GLOBAL.SQL_MODE | +--------------------------------------------+--------------------------------------------+ | NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | +--------------------------------------------+--------------------------------------------+ 1 row in set (0.00 sec)
デフォルトではNO_AUTO_CREATE_USER
とNO_ENGINE_SUBSTITUTION
が設定されており、
Strictモードの設定値であるSTRICT_ALL_TABLES
またはSTRICT_TRANS_TABLES
が設定されていません。
my.cnfのパス
my.cnfのパスを調べます。
上記には/etc/my.cnf
が一番上に書いてありますが、/etc/my.cnf
に設定ファイルを置いてみても反映されませんでした。
上記の方法を試します。
$ mysql --help | grep my.cnf /usr/local/etc/my.cnf ~/.my.cnf order of preference, my.cnf, $MYSQL_TCP_PORT, $ cat /usr/local/etc/my.cnf # # This group is read both both by the client and the server # use it for options that affect everything # [client-server] # # include all files from the config directory # !includedir /usr/local/etc/my.cnf.d
/usr/local/etc/my.cnf
に設定ファイルmy.cnf
がありました。
設定
先程の設定に!includedir /usr/local/etc/my.cnf.d
とあるので、/usr/local/etc/my.cnf
を直接編集するのではなく、/usr/local/etc/my.cnf.d
に設定ファイルを新しく作るようにします。
設定値にはデフォルトで設定されていたNO_AUTO_CREATE_USER
とNO_ENGINE_SUBSTITUTION
も含めておきます。
含めないとデフォルトで設定されていた上記2つは消えてしまいます。
Strictモードの設定値はSTRICT_ALL_TABLES
とSTRICT_TRANS_TABLES
があります。
違いについてはDifference between strict_all_tables and strict_trans_tablesがわかりやすいです。
自分は間違った値が入ってほしくなかったのでSTRICT_ALL_TABLES
を使用するようにしました。
InnoDBだと違いは無いようなのでInnoDBの場合はどちらを選んでも良いでしょう。
$ emacs /usr/local/etc/my.cnf.d/my.cnf $ cat /usr/local/etc/my.cnf.d/my.cnf [mysqld] sql_mode = NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,STRICT_ALL_TABLES # MariaDBの再起動 $ mysql.server restart Shutting down MySQL .. SUCCESS! Starting MySQL .170105 01:51:36 mysqld_safe Logging to '/usr/local/var/mysql/i.local.err'. SUCCESS!
設定値を確認してみます。
MariaDB [(none)]> SELECT @@SQL_MODE, @@GLOBAL.SQL_MODE; +--------------------------------------------------------------+--------------------------------------------------------------+ | @@SQL_MODE | @@GLOBAL.SQL_MODE | +--------------------------------------------------------------+--------------------------------------------------------------+ | STRICT_ALL_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | STRICT_ALL_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | +--------------------------------------------------------------+--------------------------------------------------------------+ 1 row in set (0.00 sec)
STRICT_ALL_TABLES
が設定されている事が確認できました。
これでNOT NULL制約が厳密に適用されます。
今日はここまで。
Raspberry PiのTimezoneをAsia/Tokyoに変更する
inabaです。
前回、GitLabのバックアップ設定を行いました。
この中で、cronで毎日バックアップをするように設定しているのですが、現状はUTC時間を基準に動くようになっています。
ですので、Timezoneの設定をAsia/Tokyoに変更していきます。
sudo raspi-config
を実行。4 Localisation Options
を選択。I2 Change Timezone
を選択。Asia
を選択。Tokyo
を選択。Finish
を選択。
Finish
を選択した後、コンソールに下記が表示されます。
Current default time zone: 'Asia/Tokyo' Local time is now: Sun Dec 25 04:00:28 JST 2016. Universal Time is now: Sat Dec 24 19:00:28 UTC 2016.
これでdate
コマンドがJSTになったと思います。
$ date Sun 25 Dec 04:01:00 JST 2016
今日はここまで。
GitLabの定期バックアップ設定
inabaです。
先日、バックアップ用のHDDをセットアップしました。
今度はGitLab側に定期バックアップの設定をしていきます。
GitLabではアプリケーションと設定のバックアップを別で行います。
バックアップディレクトリ作成
バックアップディレクトリをバックアップ用HDDのマウントディレクトリ下に作成します。
バックアップ用HDDをマウントしたディレクトリは/mnt/backups
ですのでその下にgitlab用のディレクトリを作ってバックアップします。
ディレクトリ構成はこんな感じになります。
mnt/ └── backups └── gitlab ├── app └── config
appディレクトリはアプリケーションデータのバックアップ用、configディレクトリは設定データのバックアップ用とします。
作成コマンドはこちら。
$ sudo mkdir /mnt/backups/gitlab $ sudo chown git /mnt/backups/gitlab $ sudo chgrp git /mnt/backups/gitlab $ sudo mkdir /mnt/backups/gitlab/config
/mnt/backups
はrootユーザ所有なのでsudo mkdir
で作成しますが、バックアップコマンドはgitユーザで動きますので、作ったままだとErrno::EACCES: Permission denied @ dir_s_mkdir - /mnt/backups/gitlab
というエラーが出ます。
なので、バックアップディレクトリのユーザとグループをchown
とchgrp
で変更しておきます。
app
ディレクトリはバックアップコマンドを実行すると自動で作ってくれるので作らないでおきます。
逆に設定ファイルのバックアップディレクトリconfig
は自動では作られませんので作ります。
こちらはrootのままで大丈夫です。
アプリケーションバックアップ
アプリケーションのデータをバックアップします。
バックアップコマンド
GitLabにはバックアップコマンドが用意されています。
$ sudo gitlab-rake gitlab:backup:create
このコマンドを実行すると設定したバックアップディレクトリに(Unixタイムスタンプ)_gitlab_backup.tar
というバックアップファイルが作成されます。
バックアップディレクトリのパスを設定していなければ、デフォルトのバックアップパス/var/opt/gitlab/backups
にバックアップファイルが作成されます。
基本的にはこのコマンドをcronで毎日決まった時間に実行するだけです。
設定
/etc/gitlab/gitlab.rb
を編集します。
## For setting up backups # ... gitlab_rails['backup_path'] = "/mnt/backups/gitlab/app" gitlab_rails['backup_keep_time'] = 604800
backup_path
は文字通り、バックアップディレクトリのパスの設定です。
backup_keep_time
はバックアップファイルを保持する期間の秒数です。
この後のcron設定で毎日バックアップする設定にしますので、バックアップファイルが無限に増えないようにします。
上記だと一週間でバックアップファイルが消える設定になりますので、バックアップファイルは7つまでになります。
編集後、GitLabの再設定を行います。
$ sudo gitlab-ctl reconfigure
cron設定
cronで毎日バックアップするように設定します。
$ sudo crontab -e -u root # 下記を追記します。 # 0 3 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1 # (空行)
CRON=1
は
The CRON=1 environment setting tells the backup script to suppress all progress output if there are no errors. This is recommended to reduce cron spam.
と書かれているので、ログ出力の抑制をしてくれるようです。
また、cronを書くときは最後の行に空行を入れておかないとダメなようです。
これでUTC3時にバックアップを行うように設定できました。
(一旦UTCのまま行きます。)
設定バックアップ
設定のバックアップを行います。
cron設定
設定もcronで毎日バックアップするように設定します。
$ sudo crontab -e -u root # 下記を追記します。 # 0 3 * * * umask 0077; tar cfz /mnt/backups/gitlab/config/$(date "+etc-gitlab-\%s.tgz") -C / etc/gitlab # (空行)
空行を入れるのはアプリケーションのバックアップと同じです。
また、cronでは%
をエスケープしなければならないようです。
アプリケーションと同じようにUTC3時にバックアップを行うように設定しました。
これで毎日バックアップができるようになりました。
参考: gitlab.com gitlab.com
今日はここまで。