概要
いま話題のChatGPTを利用して、ピザ注文のREST APIをGo言語の実装で作ってみましたので、その内容を紹介したいと思います。
前回の記事の続きです。
何度か繰り返すうちにコツがわかってきましたので、その内容も紹介したいと思います。
この記事で説明すること
(※前回と同じ内容です)
私がChatGPTに入力した内容と、その応答を(ほぼ)そのまま記載します。
尚、この記事内容の目的は、ChatGPTの利用で、どれくらいコーディングを効率化できるかを確認することです。このため、ChatGPTの出力内容に完璧さは求めません。
9割くらいそれっぽいものを書いてもらえれば、あとは自分でそれを直す方針です。「書こうと思えば自分で書けるけど、全部書くのは面倒だから、任せられる部分はChatGPTに任せる」目的で利用しています。
この記事で説明しないこと
実行環境の構築手順の詳細は書きません。
また、ChatGPTが回答してくれた実装が最適なものであるか、バグを含まないか、などの細かい部分は言及していません。
ポイント
検証を繰り返す中で気が付いたことです。
- 質問は日本語でもよいが、回答は英語でしてもらったほうが良い。日本語で質問して日本語で回答してもらうと、後述のようにいまいち上手くいかない。
- かつ、英語で回答してもらうと応答が早い。
- 英語で答えてくれ、はなるべく念押ししたほうが良い。途中で省略すると日本語の回答に戻る。
- 実装の手順として、先にメソッドを考えてもらって、その後、APIで書き直してもらったが、せっかくメソッドで切り出していた部分がmain関数にずらずら書く実装になってしまった。「メソッド○○を呼び出すように実装してくれ」のようにハッキリ伝えたほうが良い。(今回の記事は、ハッキリ伝えるようにした訂正版)
- 登録処理を作ってもらったが、入力値のチェックなど細かいところまでは依頼しないと実装してくれない。
日本語でうまくいかなかった例:
- SQLやソースがなどがそのまま回答部分の表現になってしまい、コピーできる形(いわゆるmarkdownのソース参照)にならなかった。
- SQLやソースがmarkdownのソース参照になった場合もあったが、途中で切れたり、ChatGPTの回答が逆にソース参照の中に入ってしまうこともあった。
- サンプルデータのINSERT文を考えてもらっても、Primary Keyの設定が漏れている回答もあった。英語だとそのあたりを間違えない。
英語で回答してもらうプロンプト
というわけで、英語で答えてもらうために、以下のプロンプトを質問に入れました。
先頭のプロンプト(※これは初回のみ):
You are an English-speaking programmer.
Please answer the questions in English.
I will ask you questions in Japanese, you answer in English.
毎回質問の前に入れるプロンプト:
Please continue your answer in English.
作ったアプリの仕様
- REST APIとして実装する。プログラムはGo言語、DBはSQLite3を利用。
- GET呼び出しでピザの一覧をjsonで返却する(あとで前回の記事のReactに繋げたい)。
- ピザの一覧は、ピザ情報のテーブルと、ピザの価格を日付の期間ごとに保持するテーブルの2つから取得する。名前による絞り込みもできるようにする。
- POSTで注文を受け付けする。
流れ
- 英語で回答することを依頼する
- テーブルを説明してDDLを書いてもらう
- サンプルデータを作ってもらう
- Go言語で今日の価格のピザを取得してもらう
- REST APIで実装してもらう
- 注文の受付も実装
実際にChatGPTとやり取りした内容
ここからが実際のやり取りです。先ほどのポイントの通り、英語で最初にプロンプトを書くことで回答を英語縛りにした上で、質問は日本語にしました。
これで十分早く答えてもらえます。
尚、実際の回答はソースコードの後に説明を付け加えてくれますが、そこはだいたい省いています。
1. 英語で回答することを依頼する
私:
You are an English-speaking programmer.
Please answer the questions in English.
I will ask you questions in Japanese, you answer in English.
ChatGPTの回答:
Sure, I'd be happy to answer your questions in English!
頼もしいですね。
よろしくお願いします。
2. テーブルを説明してDDLを書いてもらう
私:
Please continue your answer in English.
SQLはSQLiteで利用できる文法で書いてください。
テーブル構造は以下とします。
テーブル名: PIZZA_INFOS
カラム名: PIZZA_ID, NAME, CALORIE, PRODUCT_DESCRIPTION
説明: ピザの情報を保持するテーブルテーブル名: PIZZA_PRICES
カラム名: PIZZA_ID, START_DATE, END_DATE, PRICE
説明: ピザのID、期間の開始日、終了日、価格を保持するテーブルDDLを教えてください。
ChatGPTの回答:
Certainly! Here's the SQL DDL (Data Definition Language) code to create the two tables with the specified columns in SQLite:
CREATE TABLE PIZZA_INFOS (
PIZZA_ID INTEGER PRIMARY KEY,
NAME TEXT NOT NULL,
CALORIE INTEGER,
PRODUCT_DESCRIPTION TEXT
);
CREATE TABLE PIZZA_PRICES (
PIZZA_ID INTEGER NOT NULL,
START_DATE TEXT NOT NULL,
END_DATE TEXT NOT NULL,
PRICE INTEGER NOT NULL,
PRIMARY KEY (PIZZA_ID, START_DATE, END_DATE),
FOREIGN KEY (PIZZA_ID) REFERENCES PIZZA_INFOS (PIZZA_ID)
);
DDLを書いてくれました。
言わなくてもちゃんとPrimary Key、Foreign Keyを書いていてくれて偉いです。
ちなみにやり取りは省きましたが、テーブルのカラム名もChatGPTに考えてもらいました。
3. サンプルデータを作ってもらう。
私:
Please continue your answer in English.
PIZZA_INFOSを3レコード、PIZZA_PRICESを5レコードのサンプルデータのINSERT文を教えてください。
ChatGPTの回答:
Sure, here are some sample INSERT statements to add 3 records to the PIZZA_INFOS table and 5 records to the PIZZA_PRICES table:
-- Insert 3 records into PIZZA_INFOS
INSERT INTO PIZZA_INFOS (PIZZA_ID, NAME, CALORIE, PRODUCT_DESCRIPTION)
VALUES (1, 'Margherita', 700, 'Tomato sauce, mozzarella, and basil'),
(2, 'Pepperoni', 850, 'Tomato sauce, mozzarella, and pepperoni'),
(3, 'Hawaiian', 900, 'Tomato sauce, mozzarella, ham, and pineapple');
-- Insert 5 records into PIZZA_PRICES
INSERT INTO PIZZA_PRICES (PIZZA_ID, START_DATE, END_DATE, PRICE)
VALUES (1, '2022-01-01', '2022-01-31', 1000),
(1, '2022-02-01', '2022-02-28', 1100),
(1, '2022-03-01', '2022-03-31', 1200),
(2, '2022-01-01', '2022-02-28', 1200),
(2, '2022-03-01', '2022-03-31', 1300);
データを作ってくれましたが、ChatGPTの学習が去年ぐらいの情報が基になっているからっぽいですが、去年(2022年)のデータになっています。
今年に書き直してもらいます。
私:
ピザ価格のデータは2023年1月、2月、3月のデータに書き換えてください。INSERT文を書き直してください。
ChatGPTの回答:
Sure, here are the revised INSERT statements for the PIZZA_PRICES table to update the price data to reflect the months of January, February, and March 2023:
-- Update prices for January 2023
INSERT INTO PIZZA_PRICES (PIZZA_ID, START_DATE, END_DATE, PRICE)
VALUES (1, '2023-01-01', '2023-01-31', 1100),
(2, '2023-01-01', '2023-01-31', 1300),
(3, '2023-01-01', '2023-01-31', 1200);
-- Update prices for February 2023
INSERT INTO PIZZA_PRICES (PIZZA_ID, START_DATE, END_DATE, PRICE)
VALUES (1, '2023-02-01', '2023-02-28', 1200),
(2, '2023-02-01', '2023-02-28', 1400),
(3, '2023-02-01', '2023-02-28', 1300);
-- Update prices for March 2023
INSERT INTO PIZZA_PRICES (PIZZA_ID, START_DATE, END_DATE, PRICE)
VALUES (1, '2023-03-01', '2023-03-31', 1300),
(2, '2023-03-01', '2023-03-31', 1500),
(3, '2023-03-01', '2023-03-31', 1400);
書き換えてくれましたが、データが9件に増えました。まぁいいか。
ちなみに、「INSERT文を書き直してください。」と言わず、ただ「書き換えてください」と言ったら、UPDATE文を答えてきました。そういうことちゃうねんw と突っ込みたくなりました。
4.Go言語で今日の価格のピザを取得してもらう
私:
Please continue your answer in English.
引数で日付、及び、ピザ名を受け取って、その日のピザの価格を取得する処理をGo言語で実装してください。
ピザ名は中間一致にしてください。
SELECTでは一緒にピザID、ピザ名、カロリーも取得して、strunct型で返却してください。
ChatGPTの回答:
Sure, here's an example implementation in Go that accepts a date and pizza name as arguments, and returns the price of the pizza on that day along with the pizza ID, name, and calorie information:
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
type Pizza struct {
PizzaID int
PizzaName string
PizzaCalorie int
PizzaPrice int
}
func GetPizzaPriceByDateAndName(date string, name string) (*Pizza, error) {
// Connect to the SQLite database
db, err := sql.Open("sqlite3", "./pizza.db")
if err != nil {
return nil, err
}
defer db.Close()
// Prepare the SQL query to retrieve the pizza price data
stmt, err := db.Prepare(`
SELECT PIZZA_INFOS.PIZZA_ID, PIZZA_INFOS.NAME, PIZZA_INFOS.CALORIE, PIZZA_PRICES.PRICE
FROM PIZZA_INFOS
INNER JOIN PIZZA_PRICES
ON PIZZA_INFOS.PIZZA_ID = PIZZA_PRICES.PIZZA_ID
WHERE PIZZA_INFOS.NAME LIKE ?
AND ? BETWEEN PIZZA_PRICES.START_DATE AND PIZZA_PRICES.END_DATE
`)
if err != nil {
return nil, err
}
defer stmt.Close()
// Execute the query with the given parameters and retrieve the results
rows, err := stmt.Query("%"+name+"%", date)
if err != nil {
return nil, err
}
defer rows.Close()
// Iterate over the results and populate the Pizza struct with the first matching record
for rows.Next() {
var pizza Pizza
err := rows.Scan(&pizza.PizzaID, &pizza.PizzaName, &pizza.PizzaCalorie, &pizza.PizzaPrice)
if err != nil {
return nil, err
}
return &pizza, nil
}
// Return an error if no matching record was found
return nil, fmt.Errorf("No matching record found")
}
func main() {
// Call the GetPizzaPriceByDateAndName function with sample parameters
pizza, err := GetPizzaPriceByDateAndName("2023-02-15", "マルゲリータ")
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("PizzaID: %d, Name: %s, Calorie: %d, Price: %d yen\n", pizza.PizzaID, pizza.PizzaName, pizza.PizzaCalorie, pizza.PizzaPrice)
}
}
}
ちゃんとピザ情報テーブルとピザ価格テーブルをJOINして、その日の価格を取得するSQLとして実装してくれました。
頭いいなあ。
structもサクっと書いてくれて素晴らしい。
5.REST APIで実装してもらう
私:
Please continue your answer in English.
上記のGetPizzaPriceByDateAndNameを呼び出すREST APIをGo言語で実装してください。
Pizza structをjsonでRESTのクライアントに返却してください。
ChatGPTの回答:
Sure, here's an example implementation of a REST API in Go that uses the GetPizzaPriceByDateAndName function and returns a Pizza struct as JSON:
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
type Pizza struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
Calories int `json:"calories"`
}
func main() {
http.HandleFunc("/pizza", pizzaHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func pizzaHandler(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
dateStr := r.URL.Query().Get("date")
name := r.URL.Query().Get("name")
// Parse date parameter
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
http.Error(w, "Invalid date format", http.StatusBadRequest)
return
}
// Call GetPizzaPriceByDateAndName function
pizza, err := GetPizzaPriceByDateAndName(date, name)
if err != nil {
http.Error(w, "Failed to get pizza price", http.StatusInternalServerError)
return
}
// Encode pizza struct as JSON and write to response
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(pizza)
if err != nil {
http.Error(w, "Failed to encode response as JSON", http.StatusInternalServerError)
return
}
}
うんうん、サクサク書いてくれて気持ちいいですね。
ちなみに、ここまで出力してもらって、一覧じゃなくて1件だけの取得になっていることに気が付きました。(早く気づけよ)
依頼の仕方が悪かったです。もうちょっと改善の余地あり。
あと、メソッドは別のpackageで実装したほうが良いですが、そこは自分で直すことにします。
5.注文の受付も実装
私:
Please continue your answer in English.
テーブルを追加します。
テーブル名: PIZZA_ORDERS
カラム名: USER_ID, ORDER_ID, PIZZA_ID, ORDER_DATE, PRICE, QUANTITY
説明: ピザの注文情報を保持するテーブルDDLを教えてください。
ChatGPTの回答:
Sure, here's the DDL for creating the PIZZA_ORDERS table:
CREATE TABLE PIZZA_ORDERS (
USER_ID INTEGER,
ORDER_ID INTEGER PRIMARY KEY,
PIZZA_ID INTEGER,
ORDER_DATE DATE,
PRICE INTEGER,
QUANTITY INTEGER,
FOREIGN KEY (PIZZA_ID) REFERENCES PIZZA_INFOS(PIZZA_ID)
);
私:
Please continue your answer in English.
ピザの注文を受け付けるREST APIをGo言語で実装してください。
一度に複数のピザを受け取れる実装にしてください。
ChatGPTの回答:
Sure, here's an example implementation of a REST API in Go that accepts pizza orders:
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
_ "github.com/mattn/go-sqlite3"
)
// Pizza represents a pizza that can be ordered
type Pizza struct {
ID int `json:"id"`
Name string `json:"name"`
Calories int `json:"calories"`
}
// Order represents a pizza order
type Order struct {
UserID int `json:"userId"`
OrderID int `json:"orderId"`
Pizzas []Pizza `json:"pizzas"`
OrderDate time.Time `json:"orderDate"`
}
func main() {
db, err := sql.Open("sqlite3", "pizza.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
http.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var order Order
err := json.NewDecoder(r.Body).Decode(&order)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
tx, err := db.Begin()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO PIZZA_ORDERS(USER_ID, ORDER_ID, PIZZA_ID, ORDER_DATE, PRICE, QUANTITY) VALUES(?, ?, ?, ?, ?, ?)")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer stmt.Close()
for _, pizza := range order.Pizzas {
var price int
err := db.QueryRow("SELECT PRICE FROM PIZZA_PRICES WHERE PIZZA_ID = ? AND START_DATE <= ? AND END_DATE >= ?", pizza.ID, order.OrderDate, order.OrderDate).Scan(&price)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
result, err := stmt.Exec(order.UserID, order.OrderID, pizza.ID, order.OrderDate, price, 1)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if rowsAffected != 1 {
http.Error(w, fmt.Sprintf("Expected 1 row affected, got %d", rowsAffected), http.StatusInternalServerError)
return
}
}
err = tx.Commit()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
ちゃんと、オーダーした日付の価格をSELECTしてから注文テーブルに登録しています。
ここまで空気読んでくれるとは。優秀なプログラマーと言ってよいでしょう。
実際は、注文番号をどうやって決めるのかとか、存在しないピザのIDが来たときにエラーにするとか、更には在庫のチェックも要りますが、いったんOKとします。
まとめ
前回に引き続きですが、今回もChatGPTが優秀さを存分に示してくれました。
ただ、今回は実はリテイクを5~6回やっており、ある程度細かく伝えないと意図した実装にならなかったり、勝手に途中までの実装を無視して別の実装を開始したりしています。
ChatGPTを使いこなすためには、意図を過不足なく伝える能力が求められます。
そのあたりについて、どれくらい書いたら良いかを今後も調査していきたいと思います。