レビューについての雑感

  • レビューは人に対して行うものではありません。人の書いたコードに対して行うものです。
  • レビューは人を攻撃するために行うものではありません。その人の書いたコードをより良くするために行うものです。
  • レビューは自身の技術力を自慢するために行うものではありません。ただし、その知識を共有することは、行き過ぎさえなければ非常に良いことです。そして、それが必須でない限り、それを他人に強制すべきではありません。
  • コードの改変にともなうコストを意識すべきです。その機能の必要性と、それを実現するためのコストはバランスがとれているべきで、それを意識したレビューをすべきです。

かわいいスタックトレース in Perl

デバッグしているとき、スタックトレースはとても役に立つ。

でも...

f:id:taegawa:20180905234802p:plain

でも、読みたくない。

そこでちょっと見やすくしてみた。

github.com

f:id:taegawa:20180905235045p:plain

標準入力から読み込むだけのシンプルなもの。
もしそのファイルが存在すれば、中身を表示するようにした。

これで地味に作業がはかどるようになりそう。

zsh の配列の index は1から始まる

夏ですね

地下鉄の駅で電車待ってるとき、ついホームのところどころにあるエアコンの前に立っちゃいますよね!
あと、弱冷房車はあるのになんで強冷房車はないんだって時々考えてしまいますよね!

本題

というわけでまずは以下の実行結果を。

$ bash
$ ar=(Apple Banana Coconut Durian)
$ echo ${ar[1]}
Banana
$ zsh
$ ar=(Apple Banana Coconut Durian) 
$ echo ${ar[1]}
Apple

配列 ar にいくつかの要素を格納して、その index = 1 の要素を取り出すと、bashzsh で取れる要素が異なるという(汗
zshのindexは1から始まるようだ。

ところでなんで他の多くのshellが配列の添字を0から始めているのにzshは1から始めているんだろうか。
いろいろ調べてみたところ、どうやら csh(同じく1から始まる) の影響なのではないか、と。

Re: zero- vs one- based array indexing?

> Can someone give me a quick history of why zsh defaults to one-based
> array indexing? Bash and ksh appear to use zero-based indexing, and
> probably most CS types prefer that too.

Basically from csh, I think. Arrays were a bit of an afterthought in
earlier Bourne-style shells, so zsh didn't take much notice.

1から始めるようにしたことについてそんなに深く考えたわけではなさそうだ、とのこと。
実際のところどうなんだろう。

ちなみに debian で採用されている dash では、高速軽量を目的としているからなのか、そもそも配列は使えない。

set -u : 未定義の変数を使用しようとしたらエラーにする

typoなんてのは普段よくあることで、そいつに無駄な時間をとられてしまうことが日常茶飯事だ。

#!/usr/bin/env bash

MY_NAME="Miku"
MY_BIRTHDAY="10/19"
MY_HOBBY="Dance"

echo $MY_NAME
echo $MY_BIRTDAY
echo $MY_HOBBY

上記の出力結果はどうなるだろうか。上から"Miku" "10/19" "Dance"と表示されそうなものだが、実際はこうなる。

Miku

Dance

MY_BIRTHDAYが出力されていないが、これは変数名を入力し間違えているから。
こういうtypoに気付きやすくするために、set -u を指定すると良い。

#!/usr/bin/env bash

set -u

MY_NAME="Miku"
MY_BIRTHDAY="10/19"
MY_HOBBY="Dance"

echo $MY_NAME
echo $MY_BIRTDAY
echo $MY_HOBBY

上記を実行すると、以下のようなエラーとなり、該当箇所で変数を入力し間違えていることに簡単に気づける。

Miku
miku.sh: line 10: MY_BIRTDAY: unbound variable

ただ、定義済みと思い込んでいた変数をうっかり使ってしまい上記のエラーに遭遇する場合も多々ある。例えば

#!/usr/bin/env bash

set -u

PERLLIB=$PERLLIB:$HOME/mylib

...

PERLLIB は Perl においてモジュールパスを指定する環境変数だが、常に定義済みとは限らず、Perl を普段から使っている人だと未定義の場合でもつい上記のように使ってしまいエラーに遭遇しやすいかもしれない(*1)。その場合は問題の箇所のみ set -u の宣言前に移動するとよい。

(*1) 昨日のオイラ

Many-to-many関係を使う

今回は GORM で Many-to-many 関係を扱う。

複数のユーザと複数の楽曲が用意されていて、各ユーザのお気に入りの楽曲を格納できるようにする。E-R図にするとこんな感じ。

f:id:taegawa:20170114140153p:plain

とりあえずコードはこんな感じ。

package main

import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
)

const (
	dsn = "root@/association?parseTime=true&loc=Asia%2fTokyo"
)

type User struct {
	gorm.Model
	Name      string
	FavMusics []Music `gorm:"many2many:user_fav_musics;"`
}

type Music struct {
	gorm.Model
	Title string
}

func main() {
	db := prepare()
	defer db.Close()

	for _, id := range []uint{1, 2, 3} {
		var user User
		db.First(&user, id).Related(&user.FavMusics, "FavMusics")
		fmt.Println("---------------------------")
		fmt.Printf("%s さんのお気に入り曲\n", user.Name)
		for _, music := range user.FavMusics {
			fmt.Printf("曲名: %s\n", music.Title)
		}
	}
	fmt.Println("---------------------------")
}

func prepare() *gorm.DB {
	db, err := gorm.Open("mysql", dsn)
	if err != nil {
		panic("Failed to connect database")
	}
	//db.LogMode(true)
	db.DropTableIfExists(&User{}, &Music{}, "user_fav_musics")

	db.AutoMigrate(&User{})
	db.AutoMigrate(&Music{})

	prepareUsers(db)
	prepareMusics(db)
	prepareFavMusics(db)

	return db
}

func prepareUsers(db *gorm.DB) {
	var usernames = []string{"Mario", "Luigi", "Peach"}

	for _, username := range usernames {
		user := User{
			Name: username,
		}
		db.Create(&user)
	}
}

func prepareMusics(db *gorm.DB) {
	var titles = []string{
		"放課後ロマンス",
		"私Loveなオトメ",
		"ニーハイエゴイスト",
		"禁断無敵のダーリン",
		"S・M・L",
	}

	for _, title := range titles {
		music := Music{
			Title: title,
		}
		db.Create(&music)
	}
}

func prepareFavMusics(db *gorm.DB) {
	//  お気に入り楽曲
	//  {user.ID, music.ID} の組
	var favorites = [][]uint{
		{1, 1}, {1, 2}, {1, 4},
		{2, 2}, {2, 3}, {2, 5},
		{3, 2}, {3, 5},
	}
	for _, favorite := range favorites {
		var user User
		user.ID = favorite[0]

		var music Music
		music.ID = favorite[1]

		db.Model(&user).Association("FavMusics").Append(&music)
	}
}

(実行結果)

---------------------------
Mario さんのお気に入り曲
曲名: 放課後ロマンス
曲名: 私Loveなオトメ
曲名: 禁断無敵のダーリン
---------------------------
Luigi さんのお気に入り曲
曲名: 私Loveなオトメ
曲名: ニーハイエゴイスト
曲名: S・M・L
---------------------------
Peach さんのお気に入り曲
曲名: 私Loveなオトメ
曲名: S・M・L
---------------------------

Model の定義

type User struct {
	gorm.Model
	Name      string
	FavMusics []Music `gorm:"many2many:user_fav_musics;"`
}

type Music struct {
	gorm.Model
	Title string
}

モデルはユーザ(User)と楽曲(Music)のみ定義する。User 側に FavMusics というフィールドを追加し、ここにお気に入りの楽曲が格納されるようにする。また FavMusics フィールドのタグで、many-to-many 関係を使用すること、および関係テーブルの名称を上記のように宣言する。AutoMigration 使用時は GORM が関係テーブル user_fav_musics を自動的に作成してくれる。

検索

		db.First(&user, id).Related(&user.FavMusics, "FavMusics")

特定ユーザに紐づくお気に入り楽曲を取得する場合は、Related()の第2引数でフィールド名を指定する。LogMode を true にすれば、上記を実行したとき以下のようなSQL文が発行されているのが分かる。

User 検索

SELECT * 
  FROM `users`  
 WHERE `users`.deleted_at IS NULL 
    AND ((`users`.`id` = '3')) 
 ORDER BY `users`.`id` ASC LIMIT 1

User のお気に入り楽曲を検索

SELECT `musics`.* 
  FROM `musics` 
    INNER JOIN `user_fav_musics` ON `user_fav_musics`.`music_id` = `musics`.`id` 
 WHERE `musics`.deleted_at IS NULL 
    AND ((`user_fav_musics`.`user_id` IN ('3'))) 
 ORDER BY `musics`.`id` ASC

関係の追加

		var user User
		user.ID = favorite[0]

		var music Music
		music.ID = favorite[1]

		db.Model(&user).Association("FavMusics").Append(&music)

多分 Association Mode を使う方法が一番素直なのではないかと。Association() の引数で関係を定義したフィールド名を指定し、Append()で追加する。ちなみに user, music は、両者の関連を特定するために必要なフィールド(ここではID)だけ定義されていれば十分。あらかじめすべてのフィールドをDBから問い合わせておく必要はない。

INSERT INTO `user_fav_musics` (`music_id`,`user_id`) 
SELECT '1','1' 
  FROM DUAL 
 WHERE NOT EXISTS 
    (SELECT * FROM `user_fav_musics` WHERE `music_id` = '1' AND `user_id` = '1')

発行されるSQL文は上記の通り。同じ組み合わせがすでに存在する場合、二重に登録されることはないようだ。

Association で任意の外部キー名を使う

初詣行ってきました

大吉引いて幸先良いです。
あと、巫女さんが可愛かったです。

本題

GORM の Association 関連で、任意の外部キー名を使用しようとしてハマったのでメモ。

標準の外部キー名を使う方法

その前に、まずは Convention に従った外部キー名を使う方法。
これはGORMのマニュアルにも書かれている通り。今回は Belongs-to の関連で試してみる。

f:id:taegawa:20170108063437p:plain

今回は上記の通り、ユーザ(users)と、それに紐づくアイコンイメージ(images)をデータとして保持できるようにする。アイコンイメージには任意のイメージ名と、そのURLを持っている。

※以下の例では association という database を使用する。もともと同名の database があって、かつ users, images というテーブルがあると一旦削除されてしまうので注意。

package main

import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
)

const (
	dsn = "root@/association?parseTime=true&loc=Asia%2fTokyo"
)

type User struct {
	gorm.Model
	Name    string
	Image   Image
	ImageID uint
}

type Image struct {
	gorm.Model
	Name string
	Url  string
}

func main() {
	db := prepare()
	defer db.Close()

	for _, id := range []uint{1, 2, 3} {
		var user User
		fmt.Println("-------------------------------")
		db.First(&user, id).Related(&user.Image)
		fmt.Printf("名前    : %s\n", user.Name)
		fmt.Printf("アイコン:\n")
		fmt.Printf("  名称: %s\n", user.Image.Name)
		fmt.Printf("  URL : %s\n", user.Image.Url)
	}
	fmt.Println("-------------------------------")
}

func prepare() *gorm.DB {
	db, err := gorm.Open("mysql", dsn)
	if err != nil {
		panic("Failed to connect database")
	}
	db.DropTableIfExists(&User{}, &Image{})

	db.AutoMigrate(&Image{})
	db.AutoMigrate(&User{})

	var members = []map[string]string{
		{"Name": "ミク", "ImageFile": "miku.jpg"},
		{"Name": "マホ", "ImageFile": "maho.jpg"},
		{"Name": "コヒメ", "ImageFile": "kohime.jpg"},
	}

	for _, member := range members {
		image := Image{
			Name: member["Name"] + "アイコン",
			Url:  "https://image.example.com/" + member["ImageFile"],
		}
		db.Create(&image)

		user := User{
			Name:    member["Name"],
			ImageID: image.ID,
		}
		db.Create(&user)
	}

	return db
}

実行結果

-------------------------------
名前    : ミク
アイコン:
  名称: ミクアイコン
  URL : https://image.example.com/miku.jpg
-------------------------------
名前    : マホ
アイコン:
  名称: マホアイコン
  URL : https://image.example.com/maho.jpg
-------------------------------
名前    : コヒメ
アイコン:
  名称: コヒメアイコン
  URL : https://image.example.com/kohime.jpg
-------------------------------

Userのモデルは以下のように定義されている。

type User struct {
	gorm.Model
	Name    string
	Image   Image
	ImageID uint
}

ここでは、フィールド Image が Image型であることが宣言されているが、その取得元レコードへの外部キーが ImageID であることは特に明示していない。また、問い合わせで

		db.First(&user, id).Related(&user.Image)

としているが、Related()で関連レコードを取得する際にも、外部キーが ImageID であることを明示していない。

それでも Related() が問題なく外部キーを特定し関連レコードを取得できるのは、特に指示のない限り、

  1. 関連モデルの名称 + 'Id'
  2. 自身のモデルの名称 + 'Id'

を外部キーとみなすようになっているから。
ここでは Image が Image型であるため、

  1. ImageID
  2. UserID

の順に外部キーを探していき、同名のキーが見つかった時点でそれを使用する。

任意の外部キーを使う方法

ところが、外部キーの名称を (モデル名) + 'Id' 「以外」にしたいケースというのも多々ある。例えば今回はアイコンイメージのIDを ImageID というキー名で参照しているが、これはあまり良い名称とは言えない。アイコンイメージであるなら分かりやすく IconImageID としたほうがいいし、また背景画像など、同じ Image モデルを参照する別のフィールドが追加されたときは、少なくともどちらかは ImageID 以外の外部キー名を使わなければならない。

そこで User モデルの ImageID を IconImageID に変更してみる。

type User struct {
	gorm.Model
	Name        string
	IconImage   Image
	IconImageID uint
}

名称変更自体はこれで良いのだが、IconImageID というキー名はこのままでは外部キーとして認識されない。これを解決する方法はいくつかあるが、GORM ドキュメントの Association の項で触れられている範囲内では2つのやり方がある。

Related() で明示的に外部キーを指定する方法

Related() の第2引数で外部キー名を明示的に指定することができる。

        db.First(&user, id).Related(&user.IconImage, "IconImageID")

お手軽。

Association Mode を使う

モデルを宣言する際、モデル間の関連についてタグで指定することができる。

type User struct {
	gorm.Model
	Name        string
	IconImage   Image `gorm:"ForeignKey:IconImageID"`
	IconImageID uint
}

上記のようにすると、「IconImage でレコードを取得する際は、外部キーとして IconImageID を使用する」と明示的に指示できる。
そして関連レコードを取得する際は、

        db.First(&user, id).Association("IconImage").Find(&user.IconImage)

のように、Association で取得対象のフィールド名を指定すれば、タグで指定したものが外部キーとして使用される。

※修正済みのソースはこちら

Type Switch

interface{}型で宣言された変数の値の具体的な型により処理を分岐させたい、というケースが考えられる。Type Switch という仕組みでそれを実現できる。switch - case 文を使って、型ごとの処理内容を case 内に記述する。

公式ドキュメントの Effective Go にもきちんと記載されているのだが、初めて見たとき少し戸惑うので一応メモ。


例えば、ある値に「かけ算」をする以下のような関数を作るとする。

  • 数値(int)が渡された場合は、その数に指定された数を掛けた結果を返す。
  • 文字列(string)が渡された場合は、それを指定された回数だけ繰り返す。

ソースはこんな感じになる。(簡略化するため int, string 以外では nil を返すものとする)

package main

import (
	"fmt"
	"strings"
)

func multiple(val interface{}, times uint) interface{} {
	fmt.Printf("Type of val is %T\n", val)

	switch val := val.(type) {
	case int:
		return val * int(times)
	case string:
		return strings.Repeat(val, int(times))
	default:
		fmt.Printf("[WARN] Can't handle this type of value : %T\n", val)
		return nil
	}
}

func main() {
	fmt.Printf("3 * 5 = %v\n\n", multiple(3, 5))
	fmt.Printf("'逃げちゃダメだ' * 5 = %v\n\n", multiple("逃げちゃダメだ", 5))
	fmt.Printf("1.5 * 5 = %v\n\n", multiple(1.5, 5))
}

実行結果

Type of val is int
3 * 5 = 15

Type of val is string
'逃げちゃダメだ' * 5 = 逃げちゃダメだ逃げちゃダメだ逃げちゃダメだ逃げちゃダメだ逃げちゃダメだ

Type of val is float64
[WARN] Can't handle this type of value : float64
1.5 * 5 = <nil>


ポイントを一つずつ見ていくと、

func multiple(val interface{}, times uint) interface{} {

まず、引数 val (掛けられる数)を interface{} 型と宣言し、任意の型の値を受け取れるようにする。
引数 times は、掛ける数。

    switch val := val.(type) {

val.(type) というのが一見奇妙に見えるが、これは普通のキャスト。int 型にキャストするために val.(int) とするのと同じ。
これにより、interface{}型として扱われていた val が、本来の型で再代入されることになる。val の本来の型が int であるなら、 val := val.(type) によりはじめて int 型として扱われる。
(補足も参照)

なおかつ switch 文の式としてこのように書くことにより、 interface{} 型として受け取った val の本来の型が何かを判定し、case で分岐させることができる。


(補足) 以下は自分でよく理解できていないところ。

キャストを行わないと、例えば

        //  val := val.(type) を以下のように変更
	switch val.(type) {

のようにすると、val が interface{} 型のままではかけ算に使えないといったコンパイルエラーとなる。

# command-line-arguments
./multiple.go:11: invalid operation: val * int(times) (mismatched types interface {} and int)
./multiple.go:13: cannot use val (type interface {}) as type string in argument to strings.Repeat: need type assertion

ところが、結果の Type of val is ... からも分かる通り、val は別に interface{} 型ではなく、引数として受け取った時点で具体的な型を持っている。
だったらキャストは不要なんじゃないかと思うのだが、コンパイル時にそれを保証できないということなのかなぁ。
そこはちとモヤモヤしてる。