🌟

ChatGPT4でMySQLのテーブル構造からテスト用fixtureデータを生成するツール

2023/12/12に公開

これは CastingONE Advent Calendar 2023 12日目の記事です。

株式会社CastingONEでバックエンドエンジニアをしている @musle_nakayu です。
今回は、ChatGPT4でMySQLのテーブル構造からテスト用のfixtureデータを生成したお話しについて書かせていただきます。まだまだ未熟者で至らない点あるかと思いますが、優しい目で見ていただけると幸いです。

背景

普段Goで開発をしている際に、テスト用のデータとしてfixtureを扱っています。fixtureにいちいちテストデータを書いているのが面倒だなと思っていたところ、ChatGPT4で簡単にできるんじゃないかーなと思いたちツール化したお話しです。

環境情報

Mac(M1), Golang, Docker, ChatGPT4

事前に必要なもの

環境変数の設定

最初に事前に準備したAPIKeyをlocalの環境変数にセットします。

 export OPENAI_API_KEY= xxx

事前データの確認

今回usersテーブルと、hobbiesテーブル(userが持つ趣味)、それを繋げる中間テーブルのuser_hobbiesテーブルを準備しました。

CREATE TABLE
    users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        email VARCHAR(255) UNIQUE NOT NULL,
        phone_number VARCHAR(20) UNIQUE NOT NULL,
        sex int NOT NULL,
        address text NOT NULL,
        birth_date time
    )ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

CREATE TABLE hobbies (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL
    )ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

CREATE TABLE
    user_hobbies (
        id INT AUTO_INCREMENT PRIMARY KEY,
        user_id int NOT NULL,
        hobby_id int UNIQUE NOT NULL,
        CONSTRAINT `fk_user_id`
            FOREIGN KEY (`user_id`)
            REFERENCES `users` (`id`)
            ON DELETE CASCADE
            ON UPDATE CASCADE,
        CONSTRAINT `fk_hobby_id`
            FOREIGN KEY (`hobby_id`)
            REFERENCES `hobbies` (`id`)
            ON UPDATE CASCADE
    )ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

compose.ymlはこんな感じで、mysqlイメージの設定、_sqlからusersテーブルなどを定義しています。

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: yes
      MYSQL_DATABASE: dammykun
    ports:
      - "3306:3306"
    volumes:
      - ./_sql:/docker-entrypoint-initdb.d
      - ./my.cnf:/etc/mysql/conf.d/my.cnf

データ生成するスクリプトを書いていく

今回こちらのlangchaingoを使いChatGPT4に問い合わせしていきます。
※langchaingoとは:LLMを用いたアプリケーション開発を効率的に行うためのGo版ライブラリになります。

1. Localで立ち上がっているDBへの接続

package main

func connectDB() (*sqlx.DB, error) {
	dsn := "root:@tcp(localhost:3306)/dammykun?parseTime=true"
	db, err := sqlx.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("failed to connect to the MySQL database: %v", err)
	}
	return db, nil
}

まずローカルで立ち上げているmysqlへ接続するための関数connectDBを用意しています。至って簡単です。

2. スキーマ情報を取得する関数

func showSchemaInfo(db *sqlx.DB, table *string) (string, error) {
	rows, err := db.Query(fmt.Sprintf("show create table %s", *table))
	if err != nil {
		log.Fatalf("failed to show columns from the MySQL database: %v", err)
		return "", err
	}
	defer rows.Close()

	for rows.Next() {
		var tableName, schemaInfo string
		if err := rows.Scan(&tableName, &schemaInfo); err != nil {
			panic(err)
		}
		return schemaInfo, nil
	}
	return "", errors.New("not found")
}

次にmysqlのスキーマ情報を取得するための関数を用意します。ここで取得したスキーマ情報は、SystemChatMessageとしてリクエストする際に扱うデータschemaInfoを戻り値として返します。

3. ファイル出力の関数

func writeOnYamlFile(fileName string, data string) error {
	f, err := os.Create(fileName)
	_, err = f.Write([]byte(data))
	if err != nil {
		return err
	}
	return nil
}

4. LLMに問い合わせをしていく

func main() {
  // flagの設定
  num := flag.Int("n", 10, "int flag") // テストデータ生成数を指定
  table := flag.String("table", "users", "string flag") // table名を指定
  fileOutput := flag.String("file", "sample", "string flag") //出力用のファイル名を指定
  flag.Parse()

  // リクエストを行う言語モデル設定の初期化
  llm, err := openai.NewChat(openai.WithModel("gpt-4-1106-preview"))
  if err != nil {
    log.Fatal(err)
  }

  ctx := context.Background()
  content := fmt.Sprintf(
    `Please generate " + %d + " fixtures for the " + %s + "table in .yaml format with id and filled fields without the model name,
    if there is a foreign key constraint, set the value to null,
    time columns is in timedate type,
    value is based on japanese,
    and id columns are between 1 value.
    `, *num, *table)

  // ChatMessageを設定しAPIにリクエストを行う
  completion, err := llm.Call(ctx, []schema.ChatMessage{
    schema.SystemChatMessage{Content: schemaInfo},
    schema.HumanChatMessage{Content: content},
  })
  if err != nil {
    log.Fatal(err)
  }
}

最初にLLMにリクエストを行うための設定を行ます。
llm, err := openai.NewChat(openai.WithModel("gpt-4-1106-preview"))
※OpenAIで扱っているモデルの一覧と詳細については以下を参照ください。
https://platform.openai.com/docs/models/overview

次に 今回のツールの中で最も重要なポイント(めちゃくちゃ筋肉作業) になりますが、contentの中にリクエストする際の質問を定義しています。

最初にyaml形式でデータを生成してくれるようにします。
generate " + %d + " fixtures for the " + %s + "table in .yaml forma with id and filled fields without the model name

次に外部キー制約があるものに関しては、データをnullで入れるように指示を作成します。
if there is a foreign key constraint, set the value to null

今回ここがネックになって、ツール開発が思うように捗らなかった背景があります。外部キー制約があることで、他のテーブルで定義されているデータを参照しつつ
データを埋めて上げる必要があります。

ある既存のマスタデータidは1-200までだった場合、それに紐づくidを1-200の間で埋めて上げる必要がある。

それらを網羅的に考慮しテキストデータでリクエストを送ることはナンセンスな気がして
結構厳しい印象でした。そのためnullを許容しているものに関しては、シンプルにnullを入れてあげるようにします。
nullのデータに関しては、面倒ですが手入力か一括置換などで変換していくことを想定しています。

逆にnullを許容していないものに関しては、最終手段ではありますが、1などで固定値を入れるように指示してあげる方法が考えられると思います。

最後に一気にまとめていっちゃいますが、
time columns is in timedate type, value is based on japanese.
timeが付くカラム名についてはtimedate型で作るようし、valueは全体的に日本語で生成するように指示を作成。

以上がllmに投げるテキストデータになります。他にもテスト用のデータを生成する上でsql上の制約を考慮するものはたくさんあるかと思いますが、一旦今回はこのような条件で
gpt-4にリクエストしたいと思います。

課題はありますがあとはお好みで拡張していけばある程度のものはできそうな気がしている次第です。

最後にllm.Callでllmに問い合わせをおこなってレスポンスを待ちます。

ツール実行

usersテーブルのテストデータを10個作ってもらいます。

go run . -table users -n 10

レスポンス結果をファイルに出力

以下が返ってきました。

- id: 1
  name: "山田 太郎"
  email: "taro.yamada@example.com"
  phone_number: "090-1234-5678"
  sex: 1
  address: "東京都渋谷区代々木1-1-1"
  birth_date: "1985-06-15T00:00:00"

- id: 2
  name: "田中 花子"
  email: "hanako.tanaka@example.com"
  phone_number: "090-8765-4321"
  sex: 2
  address: "大阪府大阪市北区梅田2-2-2"
  birth_date: "1990-11-30T00:00:00"

- id: 3
  name: "佐藤 一郎"
  email: "ichiro.sato@example.com"
  phone_number: "080-3456-7890"
  sex: 1
  address: "京都府京都市中京区錦3-3-3"
  birth_date: "1978-02-20T00:00:00"

- id: 4
  name: "鈴木 れいな"
  email: "reina.suzuki@example.com"
  phone_number: "070-6543-2109"
  sex: 2
  address: "北海道札幌市中央区南4-4-4"
  birth_date: "1992-07-10T00:00:00"

- id: 5
  name: "伊藤 明美"
  email: "akemi.ito@example.com"
  phone_number: "090-9988-7766"
  sex: 2
  address: "宮城県仙台市青葉区国分町5-5-5"
  birth_date: "1986-03-05T00:00:00"

- id: 6
  name: "小林 拓也"
  email: "takuya.kobayashi@example.com"
  phone_number: "080-6677-5566"
  sex: 1
  address: "福岡県福岡市博多区博多駅前6-6-6"
  birth_date: "1995-09-23T00:00:00"

- id: 7
  name: "高橋 真樹"
  email: "maki.takahashi@example.com"
  phone_number: "070-4455-3322"
  sex: 2
  address: "広島県広島市中区本通7-7-7"
  birth_date: "1983-12-12T00:00:00"

- id: 8
  name: "中村 涼子"
  email: "ryoko.nakamura@example.com"
  phone_number: "090-2244-6688"
  sex: 2
  address: "神奈川県横浜市西区みなとみらい8-8-8"
  birth_date: "1989-04-29T00:00:00"

- id: 9
  name: "斎藤 翼"
  email: "tsubasa.saito@example.com"
  phone_number: "080-5599-2244"
  sex: 1
  address: "愛知県名古屋市中区栄9-9-9"
  birth_date: "2001-05-14T00:00:00"

- id: 10
  name: "井上 良子"
  email: "ryoko.inoue@example.com"
  phone_number: "070-6678-5567"
  sex: 2
  address: "沖縄県那覇市久茂地10-10-10"
  birth_date: "1997-01-27T00:00:00"

指示通りの綺麗なデータが生成できています。このデータについては結構良さそう!外部キーに関しては、今回テーブルに存在していないため、もう一個のほうのテーブルも指定して実行してみます。

go run . -table user_hobbies -n 5
- id: 1
  user_id: null
  hobby_id: null
  created_at: 2023-04-01T14:00:00+09:00
  updated_at: 2023-04-01T14:00:00+09:00

- id: 2
  user_id: null
  hobby_id: null
  created_at: 2023-04-02T15:30:00+09:00
  updated_at: 2023-04-02T15:30:00+09:00

- id: 3
  user_id: null
  hobby_id: null
  created_at: 2023-04-03T16:45:00+09:00
  updated_at: 2023-04-03T16:45:00+09:00

- id: 4
  user_id: null
  hobby_id: null
  created_at: 2023-04-04T17:20:00+09:00
  updated_at: 2023-04-04T17:20:00+09:00

- id: 5
  user_id: null
  hobby_id: null
  created_at: 2023-04-05T18:00:00+09:00
  updated_at: 2023-04-05T18:00:00+09:00

指示通り帰ってきていそうですが、外部キーとして該当するidの箇所はすべてnullで埋められているため外部キー制約もつカラムが複数あるテーブル、また大量でidに多様な値が入るデータを生成したい場合は課題が残りました。

まとめ

ChatGPTに投げるテキスト次第で、簡単にスキーマ構造を考慮した、ある程度の簡易データは返せることがわかりました。

また複雑なスキーマに関しては、言語化するのは結構な筋肉作業になりそうだったため今回は断念しています。

今回はスタートとしてfixtureデータが生成できる簡易ツールを作成してみましたが、今後進捗や良いご意見があれば第二弾としてv2を作りたいと考えておりますので、たくさんのご意見お待ちしております!

CastingONEのご紹介

株式会社CastingONE では、OSSを使うだけでなくコントリビュートしてコミュニティに還元していくソフトウェアエンジニアを募集しております!
Goエンジニア│導入実績1,100社以上の自社プロダクト開発担当 - 株式会社CastingONEのWebエンジニアの採用 - Wantedly
バックエンドエンジニア│モダンな開発環境で経験を積みたい方歓迎! - 株式会社CastingONEのWebエンジニアの採用 - Wantedly

Discussion