การ CRUD กับฐานข้อมูล ด้วย sqlc


ในนี้เราจะมาเรียนรู้วิธีการเขียนโค้ด Go เพื่อดำเนินการ CRUD บนฐานข้อมูล

CRUD คืออะไร?


มันคือ 4 ปฏิบัติการพื้นฐาน:

  • C ย่อมาจาก Create, หรือแทรกระเบียนใหม่ลงในฐานข้อมูล
  • R ใช้สำหรับ Read ซึ่งหมายถึงการดึงบันทึกจากฐานข้อมูล
  • U คือ Update การเปลี่ยนเนื้อหาของบันทึกในฐานข้อมูล
  • และ D เป็นการ Delete ลบบันทึกออกจากฐานข้อมูล


Library ใดที่จะใช้?


มีหลายวิธีในการดำเนินการ CRUD ใน Go

แพ็คเกจมาตรฐาน database/sql


อย่างแรกคือการใช้แพ็คเกจฐานข้อมูลไลบรารีมาตรฐาน / แพ็คเกจ database/sql


ดังที่คุณเห็นในตัวอย่างนี้ เราเพียงแค่ใช้ฟังก์ชัน QueryRowContext() ส่งผ่าน SQL query และพารามิเตอร์บางตัว จากนั้นเราสแกนผลลัพธ์เป็นตัวแปรเป้าหมาย

ข้อได้เปรียบหลักของวิธีนี้คือทำงานได้เร็วมาก และเขียนโค้ดได้ค่อนข้างตรงไปตรงมา

อย่างไรก็ตาม ข้อเสียของมันคือ เราต้องแมปฟิลด์ SQL กับตัวแปรด้วยตนเอง ซึ่งค่อนข้างน่าเบื่อและง่ายต่อการทำผิดพลาด หากลำดับของตัวแปรไม่ตรงกัน หรือหากเราลืมส่งผ่านอาร์กิวเมนต์ไปยังการเรียกใช้ฟังก์ชัน ข้อผิดพลาดจะแสดงขึ้นเมื่อรันไทม์เท่านั้น


Gorm


อีกวิธีหนึ่งคือการใช้ Gorm ซึ่งเป็นไลบรารี Mapping เชิงวัตถุระดับสูงสำหรับ Go

มันสะดวกมากที่จะใช้เพราะการดำเนินการ CRUD ทั้งหมด ได้ถูกนำมาใช้แล้ว ดังนั้นการเขียนโค้ดของเราจึงสั้นมาก เนื่องจากเราจำเป็นต้องประกาศแบบจำลองและเรียกใช้ฟังก์ชันที่ Gorm ให้มาเท่านั้น


ดังที่คุณเห็นในโค้ดตัวอย่างเหล่านี้ เรามีฟังก์ชัน Create() สำหรับสร้างเรกคอร์ด และหลายฟังก์ชันในการดึงข้อมูล เช่น First(), Take(), Last(), Find().

มันดูเท่มาก แต่ปัญหาคือ เราต้องเรียนรู้วิธีเขียนข้อความค้นหาโดยใช้ฟังก์ชันของ gorm ที่จัดเตรียมไว้ให้ มันจะน่ารำคาญถ้าเราไม่รู้ว่าจะใช้ฟังก์ชั่นใด

โดยเฉพาะอย่างยิ่งเมื่อเรามีคำถามที่ซับซ้อนซึ่งจำเป็นต้องมีการเข้าร่วมตาราง เราต้องเรียนรู้วิธีประกาศแท็กการเชื่อมโยงเพื่อให้ gorm เข้าใจความสัมพันธ์ระหว่างตาราง เพื่อให้สามารถสร้าง SQL query ที่ถูกต้องได้

สำหรับฉัน ฉันชอบเขียน SQL query ด้วยตัวเอง มีความยืดหยุ่นมากกว่า และฉันสามารถควบคุมสิ่งที่ฉันต้องการให้ฐานข้อมูลทำทั้งหมดได้

ข้อกังวลหลักประการหนึ่งเมื่อใช้ gorm คือมันทำงานช้ามากเมื่อมี traffic ที่หนาแน่น มีเกณฑ์มาตรฐานบนอินเทอร์เน็ตซึ่งแสดงให้เห็นว่า gorm สามารถทำงานช้ากว่า library มาตรฐาน 3-5 เท่า


sqlx


ด้วยเหตุนี้ หลายคนจึงเปลี่ยนไปใช้แนวทางกลางซึ่งใช้ไลบรารี sqlx

มันทำงานเกือบจะเร็วพอๆ กับไลบรารีมาตรฐาน และใช้งานง่ายมาก การแมปฟิลด์ทำได้โดยใช้ข้อความค้นหาหรือแท็กโครงสร้าง


มันมีฟังก์ชั่นบางอย่างเช่น Select() or StructScan() ซึ่งจะสแกนผลลัพธ์ลงในฟิลด์ struct โดยอัตโนมัติ ดังนั้นเราจึงไม่ต้อง mapping ด้วยตนเองเหมือนในแพ็คเกจ database/sql ซึ่งจะช่วยให้โค้ดสั้นลง และลดข้อผิดพลาดที่อาจเกิดขึ้นได้

อย่างไรก็ตามโค้ดที่เราต้องเขียนนั้นยังค่อนข้างยาว และข้อผิดพลาดใดๆ ในการสืบค้นจะถูกตรวจจับได้เฉพาะตอนรันไทม์เท่านั้น

แล้วมีวิธีอื่นที่ดีกว่านี้ไหม?


sqlc


คำตอบคือ sqlc !

database/sql มันทำงานเร็ว เหมือนกันมาก มันใช้งานง่ายสุด ๆ และสิ่งที่น่าตื่นเต้นที่สุดคือ เราแค่ต้องเขียนคำสั่ง SQL จากนั้นโค้ด CRUD ของ Go จะถูกสร้างขึ้นโดยอัตโนมัติสำหรับเรา

ดังที่คุณเห็นในตัวอย่างนี้ เราเพียงแค่ส่ง db schema และคำสั่ง SQL ไปยัง sqlc แต่ละ query มี 1 comment เพื่อบอกให้ sqlc สร้างลายเซ็นฟังก์ชันที่ถูกต้อง

จากนั้น sqlc จะสร้างโค้ด Go สำนวนซึ่งใช้ไลบรารี มาตรฐาน database/sql

และเนื่องจาก sqlc แยกวิเคราะห์การสืบค้น SQL เพื่อให้เข้าใจว่ามันทำอะไรเพื่อสร้างโค้ดให้เรา ดังนั้นข้อผิดพลาดใดๆ จะถูกตรวจจับและรายงานทันที ฟังดูน่าทึ่งใช่มั้ย?

ปัญหาเดียวที่ฉันพบใน sqlc คือในขณะนี้รองรับ Postgres อย่างสมบูรณ์เท่านั้น MySQL ยังคงทดลองอยู่ ดังนั้น หากคุณใช้ Postgres ในโครงการของคุณ ฉันคิดว่า sqlc เป็นเครื่องมือที่เหมาะสมที่จะใช้


ข้อกำหนดเบื้องต้น


ข้อกำหนดสำหรับบทความนี้คือ คุณต้องติดตั้ง Go บนคอมพิวเตอร์ของคุณ และได้ทำตามบทความ การย้ายฐานข้อมูลด้วย Go บน Mac  มาก่อน



ติดตั้ง sqlc


ตอนนี้ฉันจะแสดงวิธีติดตั้งและใช้งาน sqlc เพื่อสร้างโค้ด CRUD สำหรับโครงการ Simple Bank ที่เรียบง่ายของเรา

ก่อนอื่นเราเปิดหน้า github จากนั้นค้นหา “installation”


ฉันใช้ Mac ดังนั้นฉันจะใช้ Homebrew มาคัดลอกคำสั่ง brew install แล้วรันในเทอร์มินัล:

brew install kyleconroy/sqlc/sqlc

เราสามารถเรียกใช้ sqlc version เพื่อดูว่ามันใช้รุ่นอะไรอยู่ ในกรณีของฉันคือเวอร์ชัน 1.8.0

sqlc version

ใช้คำสั่ง sqlc help เรียนรู้การใช้งานกัน

sqlc help
  • ขั้นแรกเรามีคำสั่งคอมไพล์เพื่อตรวจสอบไวยากรณ์ของ SQL และพิมพ์ข้อผิดพลาด
  • จากนั้นสร้างคำสั่งที่สำคัญที่สุด มันจะทำทั้งการตรวจสอบข้อผิดพลาดและสร้างโค้ด Go จากการสืบค้น SQL สำหรับเรา
  • นอกจากนี้เรายังมีคำสั่ง init เพื่อสร้างไฟล์การตั้งค่า slqc.yaml ที่ว่างเปล่า


เขียนไฟล์การตั้งค่า


ตอนนี้ฉันจะไปที่โฟลเดอร์โครงการ simple_bank ที่เราได้ดำเนินการในบทความ การย้ายฐานข้อมูลด้วย Go บน Mac ครั้งก่อน:


และสร้างไฟล์ sqlc.yaml ด้วยคำสั่ง

sqlc init

เราสามารถดูไฟล์ sqlc.yaml ตอนนี้ค่อนข้างว่าง กลับไปที่หน้า sqlc github เลือก Settings และค้นหาการตั้งค่า


ให้คัดลอกรายการการตั้งค่าแล้ววางลงในไฟล์ sqlc.yaml

เราสามารถบอกให้ sqlc สร้างแพ็คเกจ Go หลายแพ็คเกจได้ แต่พูดง่ายๆ คือ ตอนนี้ฉันแค่ใช้แพ็คเกจเดียวในตอนนี้

version: "1"
packages:
  - name: "db"
    path: "./db/sqlc"
    queries: "./db/query/"
    schema: "./db/migration/"
    engine: "postgresql"
    emit_json_tags: true
    emit_prepared_queries: false
    emit_interface: false
    emit_exact_table_names: false


  • name ตัวเลือกที่นี่คือบอก sqlc ว่าแพ็คเกจ Go ที่จะถูกสร้างขึ้นคืออะไร ฉันคิดว่า db เป็นชื่อแพ็คเกจที่ดี

  • ต่อไป เราต้องระบุ path ไปยังโฟลเดอร์เพื่อเก็บไฟล์โค้ด Go ที่สร้างขึ้น ฉันจะสร้างโฟลเดอร์ใหม่ sqlc ภายในโฟลเดอร์ db และเปลี่ยนสตริง path นี้เป็น ./db/sqlc

  • จากนั้นเรามีตัวเลือก queries ที่จะบอก sqlc ว่าจะค้นหาไฟล์ SQL query ได้ที่ไหน มาสร้างโฟลเดอร์ใหม่ query ภายในโฟลเดอร์ db กัน แล้วเปลี่ยนค่านี้เป็น ./db/query

  • ในทำนองเดียวกัน ตัวเลือกสคีมานี้ควรชี้ไปที่โฟลเดอร์ที่มีสคีมาฐานข้อมูลหรือไฟล์การย้ายข้อมูล ในกรณีของเราก็คือ ./db/migration

  • ตัวเลือกต่อไปคือ engine การบอก sqlc ว่าเอ็นจิ้นฐานข้อมูลใดที่เราต้องการใช้ เรากำลังใช้ Postgresql สำหรับโครงการ simple_bank ของเรา

  • ที่นี่เราตั้งค่าเป็น emit_json_tags เป็น true เนื่องจาก เราต้องการให้ sqlc เพิ่มแท็ก JSON ให้กับโครงสร้างที่สร้างขึ้น

  • emit_prepared_queries คือคำสั่ง sqlc ให้สร้างโค้ดที่ทำงานกับคำสั่งที่เตรียมไว้ ในขณะนี้ เรายังไม่จำเป็นต้องเพิ่มประสิทธิภาพการทำงาน ดังนั้นเรามาตั้งค่านี้เป็น false เพื่อให้ง่าย
  • จากนั้น emit_interface ตัวเลือกในการบอก sqlc เพื่อสร้าง Querier อินเทอร์เฟซสำหรับแพ็คเกจที่สร้างขึ้น อาจมีประโยชน์ในภายหลังถ้าเราต้องการจำลองฐานข้อมูลเพื่อทดสอบฟังก์ชันระดับสูงกว่า สำหรับตอนนี้ ให้ตั้งค่าเป็น false

  • ตัวเลือกสุดท้ายคือ emit_exact_table_names. โดยค่าเริ่มต้น ค่านี้ false คือ Sqlc จะพยายามทำให้ชื่อตารางเป็นเอกพจน์เพื่อใช้เป็นชื่อโครงสร้างของโมเดล ตัวอย่างเช่น accounts ตารางจะกลายเป็น Accountstruct หากคุณตั้งค่าตัวเลือกนี้เป็น true ชื่อโครงสร้างจะเป็นชื่อ Accounts แทน ฉันคิดว่าชื่อเอกพจน์ดีกว่าเพราะวัตถุประเภทเดียว Accounts ในรูปพหูพจน์อาจสับสนว่าเป็นวัตถุหลายชิ้น


เรียกใช้ sqlc สร้างคำสั่ง


เรียกใช้คำสั่ง

sqlc generate


เรามีข้อผิดพลาดเนื่องจากยังไม่มีการ queries ใน query folder

เราจะเขียน query ในอีกสักครู่ ในตอนนี้ เรามาเพิ่ม sqlc คำสั่งใหม่ให้กับไฟล์ Makefile. จะช่วยให้เพื่อนร่วมทีมของเราค้นหาคำสั่งทั้งหมดที่สามารถใช้สำหรับการพัฒนาได้อย่างง่ายดายในที่เดียว

postgres:
	docker run --name postgres12 -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:12-alpine

createdb:
	docker exec -it postgres12 createdb --username=root --owner=root simple_bank

dropdb:
	docker exec -it postgres12 dropdb simple_bank

migrateup:
	migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose up

migratedown:
	migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose down

sqlc:
	sqlc generate


.PHONY: postgres createdb dropdb migrateup migratedown sqlc


สร้างการดำเนินการ


ตอนนี้ มาเขียน SQL query แรกของเรา เพื่อสร้าง account โดยจะสร้างไฟล์ใหม่ account.sql ภายในโฟลเดอร์ db/query

จากนั้นกลับไปที่หน้า sqlc github และค้นหาไฟล์ getting started



เขียน SQL query เพื่อสร้าง account


ต่อไปนี้เราจะเห็นตัวอย่างบางส่วนว่า SQL query ควรมีลักษณะอย่างไร ให้คัดลอกคำสั่ง CreateAuthor และวางลงในไฟล์ account.sql ของเรา

เป็นเพียงพื้นฐาน INSERT query สิ่งเดียวที่พิเศษคือความคิดเห็นที่อยู่ด้านบน ความคิดเห็นนี้จะแนะนำ sqlc วิธีสร้างลายเซ็นฟังก์ชัน Go สำหรับ SQL queryนี้

ในกรณีของเรา ชื่อของฟังก์ชันจะเป็น CreateAccount. และมันควรส่งคืน 1 Account วัตถุเดียว ดังนั้นเราจึงมีป้ายกำกับ :one ที่นี่

-- name: CreateAccount :one
INSERT INTO accounts (
  owner,
  balance,
  currency
) VALUES (
  $1, $2, $3
) RETURNING *;

เราไม่จำเป็นต้องระบุ id เนื่องจากเป็นคอลัมน์เพิ่มค่าอัตโนมัติ ทุกครั้งที่มีการแทรกระเบียนใหม่ ฐานข้อมูลจะเพิ่มหมายเลขลำดับของรหัสบัญชีโดยอัตโนมัติ และใช้เป็นค่าของ id คอลัมน์

คอลัมน์ created_at จะถูกเติมโดยอัตโนมัติด้วยค่าเริ่มต้น ซึ่งเป็นเวลาที่สร้างเรกคอร์ด

ดังนั้น เราจำเป็นต้องระบุค่าสำหรับ owner, balance, และ currency เท่านั้น มี 3 คอลัมน์ ดังนั้นเราต้องส่ง 3 อาร์กิวเมนต์เข้าไปในอนุประโยค VALUES

สุดท้าย มีการใช้ RETURNING *อนุประโยคเพื่อบอกให้ Postgres คืนค่าของคอลัมน์ทั้งหมดหลังจากแทรกบันทึกลงในตารางบัญชี (รวมถึง id และ created_at) สิ่งนี้สำคัญมาก เพราะหลังจากสร้างบัญชีแล้ว เรามักจะต้องการคืน ID ให้กับลูกค้าเสมอ


สร้างโค้ด Go เพื่อสร้าง account


เปิดเทอร์มินัลแล้วเรียกใช้ make sqlc กัน

make sqlc

ในโฟลเดอร์ db/sqlc เราจะเห็นไฟล์ที่สร้างขึ้นใหม่ 3 ไฟล์

ไฟล์ที่ 1 คือ models.go ไฟล์นี้มีคำจำกัดความโครงสร้างของ 3 model คือ: Account, Entry และ Transfer

// Code generated by sqlc. DO NOT EDIT.

package db

import (
	"time"
)

type Account struct {
	ID        int64     `json:"id"`
	Owner     string    `json:"owner"`
	Balance   int64     `json:"balance"`
	Currency  string    `json:"currency"`
	CreatedAt time.Time `json:"created_at"`
}

type Entry struct {
	ID        int64 `json:"id"`
	AccountID int64 `json:"account_id"`
	// can be negative or positive
	Amount    int64     `json:"amount"`
	CreatedAt time.Time `json:"created_at"`
}

type Transfer struct {
	ID            int64 `json:"id"`
	FromAccountID int64 `json:"from_account_id"`
	ToAccountID   int64 `json:"to_account_id"`
	// must be positive
	Amount    int64     `json:"amount"`
	CreatedAt time.Time `json:"created_at"`
}

พวกเขาทั้งหมดมีแท็ก JSON เนื่องจากเรากำลังตั้ง emit_json_tags ค่า trueใน sqlc.yaml ฟิลด์ Amount ของ Entry และ Transferstruct ยังมีความคิดเห็นอยู่ด้านบน เนื่องจากเราได้เพิ่มไว้ในคำจำกัดความสคีมาของฐานข้อมูลในการบรรยายครั้งก่อน


ไฟล์ที่ 2 คือ db.go ไฟล์นี้มี DBTX อินเทอร์เฟซ มันกำหนด 4 วิธีทั่วไปที่ทั้งคู่ sql.DB และ sql.Tx มี object ซึ่งช่วยให้เราใช้ฐานข้อมูลหรือธุรกรรมเพื่อดำเนินการสืบค้นข้อมูลได้อย่างอิสระ

// Code generated by sqlc. DO NOT EDIT.

package db

import (
	"context"
	"database/sql"
)

type DBTX interface {
	ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
	PrepareContext(context.Context, string) (*sql.Stmt, error)
	QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
	QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}

func New(db DBTX) *Queries {
	return &Queries{db: db}
}

type Queries struct {
	db DBTX
}

func (q *Queries) WithTx(tx *sql.Tx) *Queries {
	return &Queries{
		db: tx,
	}
}

ดังที่คุณเห็นในที่นี้ฟังก์ชัน New() รับ DBTX เป็นอินพุตและส่งคืนอ็อบเจ็กต์ Queries ดังนั้นเราจึงสามารถส่งผ่านใน object sql.DB หรือ sql.Tx ขึ้นอยู่กับว่าเราต้องการดำเนินการค้นหาเพียง 1 แบบสอบถามเดียวหรือชุดของแบบสอบถามหลายรายการภายในธุรกรรม

นอกจากนี้ยังมีเมธอด WithTx() ซึ่งช่วยให้อินสแตนซ์ Queries เชื่อมโยงกับธุรกรรมได้ เราจะเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ในการบรรยายเกี่ยวกับการทำธุรกรรมอื่น

ไฟล์ที่ 3 เป็นไฟล์ account.sql.go

// Code generated by sqlc. DO NOT EDIT.
// source: account.sql

package db

import (
	"context"
)

const createAccount = `-- name: CreateAccount :one
INSERT INTO accounts (
  owner,
  balance,
  currency
) VALUES (
  $1, $2, $3
) RETURNING id, owner, balance, currency, created_at
`

type CreateAccountParams struct {
	Owner    string `json:"owner"`
	Balance  int64  `json:"balance"`
	Currency string `json:"currency"`
}

func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) {
	row := q.db.QueryRowContext(ctx, createAccount, arg.Owner, arg.Balance, arg.Currency)
	var i Account
	err := row.Scan(
		&i.ID,
		&i.Owner,
		&i.Balance,
		&i.Currency,
		&i.CreatedAt,
	)
	return i, err
}

const getAccount = `-- name: GetAccount :one
SELECT id, owner, balance, currency, created_at FROM accounts
WHERE id = $1 LIMIT 1
`

func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
	row := q.db.QueryRowContext(ctx, getAccount, id)
	var i Account
	err := row.Scan(
		&i.ID,
		&i.Owner,
		&i.Balance,
		&i.Currency,
		&i.CreatedAt,
	)
	return i, err
}

const listAccounts = `-- name: ListAccounts :many
SELECT id, owner, balance, currency, created_at FROM accounts
ORDER BY id
LIMIT $1
OFFSET $2
`

type ListAccountsParams struct {
	Limit  int32 `json:"limit"`
	Offset int32 `json:"offset"`
}

func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) {
	rows, err := q.db.QueryContext(ctx, listAccounts, arg.Limit, arg.Offset)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Account
	for rows.Next() {
		var i Account
		if err := rows.Scan(
			&i.ID,
			&i.Owner,
			&i.Balance,
			&i.Currency,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

ชื่อแพ็คเกจเป็น db เป็นไปตามที่เรากำหนดในไฟล์ sqlc.yaml

ที่ด้านบน เราจะเห็นการสร้างบัญชีแบบสอบถาม SQL เกือบจะเหมือนกับที่เราเขียนใน ไฟล์ account.sql ยกเว้นส่วน RETURN คำสั่ง Sqlc ได้แทนที่ RETURN *ด้วยชื่อของคอลัมน์ทั้งหมดอย่างชัดเจน ซึ่งจะทำให้แบบสอบถามชัดเจนขึ้นและหลีกเลี่ยงการสแกนค่าในลำดับที่ไม่ถูกต้อง

จากนั้น เราก็มี CreateAccountParamsstruct ซึ่งประกอบด้วยคอลัมน์ทั้งหมดที่เราต้องการตั้งค่าเมื่อเราสร้างบัญชีใหม่: owner, balance, currency.

ฟังก์ชั่น CreateAccount() ถูกกำหนดให้เป็นวิธีการของอ็อบเจ็กต์ Queries มีชื่อนี้เนื่องจากเราได้สั่ง sqlc ด้วยความคิดเห็นในแบบสอบถาม SQL ของเรา ฟังก์ชันนี้รับบริบทและอ็อบเจ็กต์ CreateAccountParams เป็นอินพุต และส่งคืน อ็อบเจ็กต์ Account model หรือ error


มาเปิดเทอร์มินัลแล้วเรียกใช้:

go mod init simplebank


ชื่อโมดูลของเราคือ simplebank ตอนนี้ไฟล์ go.mod ถูกสร้างขึ้นสำหรับเรา เรียกใช้คำสั่งต่อไปนี้เพื่อติดตั้ง dependencies

go mod tidy

ใน CreateAccount() ฟังก์ชันนี้ เราเรียก QueryRowContext()ให้รันคำสั่ง create-account SQL ฟังก์ชันนี้เป็นของอินเทอร์เฟซ DBTX ที่เราเคยเห็นมาก่อน เราส่งต่อบริบท การสืบค้น และอาร์กิวเมนต์ 3 รายการ: , owner, balance และ currency

ฟังก์ชันส่งคืนวัตถุแถวที่เราสามารถใช้สแกนค่าของแต่ละคอลัมน์เป็นตัวแปรที่ถูกต้อง นี่คือรหัสพื้นฐานที่เรามักจะต้องเขียนด้วยตนเองหากเราใช้ database/sql ไลบรารี มาตรฐาน แต่มันเจ๋งแค่ไหนที่จะสร้างมันขึ้นมาโดยอัตโนมัติสำหรับเรา!

สิ่งที่น่าทึ่งอีกอย่างเกี่ยวกับ sqlc คือ ตรวจสอบไวยากรณ์การสืบค้น SQL ก่อนสร้างรหัส ดังนั้นที่นี่หากฉันพยายามลบอาร์กิวเมนต์ที่ 3 ในแบบสอบถาม


และเรียกใช้ make มีการรายงานข้อผิด พลาด db/query/account.sql:1:1: syntax error at or near “)”

sqlc generate

ด้วยเหตุนี้ หาก sqlc สร้างรหัสได้สำเร็จ เราจึงมั่นใจได้ว่าไม่มีข้อผิดพลาดที่โง่เขลาในการสืบค้น SQL ของเรา

สิ่งสำคัญอย่างหนึ่งเมื่อทำงานกับ sqlc คือเราไม่ควรแก้ไขเนื้อหาของไฟล์ที่สร้างขึ้น เพราะทุกครั้งที่เราเรียกใช้ sqlc ไฟล์เหล่านั้นทั้งหมดจะถูกสร้างใหม่ และการเปลี่ยนแปลงของเราจะสูญหายไป ดังนั้น อย่าลืมสร้างไฟล์ใหม่ หากคุณต้องการเพิ่มโค้ดเพิ่มเติมในแพ็คเกจ db

เอาล่ะ ตอนนี้เรารู้วิธีสร้างบันทึกในฐานข้อมูลแล้ว


การดำเนินการอ่าน (GET/LIST)


ไปที่การดำเนินการต่อไป: READ.

ในตัวอย่างนี้ มีการสืบค้นข้อมูลพื้นฐาน 2 คำสั่ง: Get และ Listล องคัดลอกไปยังไฟล์ account.sqlของเรา

เขียนแบบสอบถาม SQL เพื่อรับ/แสดงรายการบัญชี

แบบสอบถามรับใช้เพื่อรับ 1 บันทึกบัญชีตามรหัส เลยจะเปลี่ยนชื่อเป็น GetAccount. และคำถามจะเป็น:

-- name: GetAccount :one
SELECT * FROM accounts
WHERE id = $1 LIMIT 1;

เราใช้ LIMIT 1 ที่นี่เพราะเราต้องการเลือก 1 ระเบียนเดียว

ListAccounts การดำเนินการต่อ ไปคือ มันจะส่งคืนบันทึกบัญชีหลายรายการ ดังนั้นเราจึงใช้ป้ายกำกับ :many ที่นี่

-- name: ListAccounts :many
SELECT * FROM accounts
ORDER BY id
LIMIT $1
OFFSET $2;

คล้ายกับแบบสอบถาม GetAccount เราเลือกจากตารางบัญชี แล้วเรียงลำดับระเบียนตามรหัสของพวกเขา

เนื่องจากในฐานข้อมูลสามารถมีได้หลายบัญชี เราจึงไม่ควรเลือกทั้งหมดพร้อมกัน เราจะทำการแบ่งหน้าแทน ดังนั้นเราจึงใช้ LIMITเพื่อกำหนดจำนวนแถวที่เราต้องการ และใช้ OFFSET เพื่อบอกให้ Postgres ข้ามแถวหลายๆ แถวนี้ก่อนที่จะเริ่มส่งคืนผลลัพธ์


สร้างโค้ด Go เพื่อรับ/แสดงรายการบัญชี


ตอนนี้ ให้เรียกใช้ make sqlc เพื่อสร้างโค้ดใหม่

make sqlc


และเปิดไฟล์ account.sql.go ไปที่ฟังก์ชัน GetAccount() และ ListAccounts() ถูกสร้างขึ้นเหมือนเมื่อก่อน sqlc ได้แทนที่ SELECT * ด้วยชื่อคอลัมน์ที่ชัดเจนสำหรับเรา

const getAccount = `-- name: GetAccount :one
SELECT id, owner, balance, currency, created_at FROM accounts
WHERE id = $1 LIMIT 1
`

func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
  row := q.db.QueryRowContext(ctx, getAccount, id)
  var i Account
  err := row.Scan(
    &i.ID,
    &i.Owner,
    &i.Balance,
    &i.Currency,
    &i.CreatedAt,
    )
  return i, err
}


ฟังก์ชัน นี้GetAccount()ใช้บริบทและรหัสบัญชีเป็นข้อมูลเข้า และภายในก็เรียกQueryRowContext()ใช้ด้วยแบบสอบถาม SQL ดิบและรหัสบัญชี โดยจะสแกนแถวในออบเจ็กต์บัญชีและส่งคืนให้กับผู้โทร ค่อนข้างง่าย!

ฟังก์ชัน ListAccounts ซับซ้อนขึ้นเล็กน้อย ใช้บริบท limit และพารามิเตอร์ offset เป็นอินพุตและส่งคืนรายการของอ็อบเจ็กต์ Account

const listAccounts = `-- name: ListAccounts :many
SELECT id, owner, balance, currency, created_at FROM accounts
ORDER BY id
LIMIT $1
OFFSET $2
`

type ListAccountsParams struct {
  Limit  int32 `json:"limit"`
  Offset int32 `json:"offset"`
}

func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) {
  rows, err := q.db.QueryContext(ctx, listAccounts, arg.Limit, arg.Offset)
  if err != nil {
    return nil, err
  }
  defer rows.Close()
  var items []Account
  for rows.Next() {
    var i Account
    if err := rows.Scan(
      &i.ID,
      &i.Owner,
      &i.Balance,
      &i.Currency,
      &i.CreatedAt,
    ); err != nil {
      return nil, err
    }
    items = append(items, i)
  }
  if err := rows.Close(); err != nil {
    return nil, err
  }
  if err := rows.Err(); err != nil {
    return nil, err
  }
  return items, nil

ข้างในมันเรียก QueryContext(), ผ่านในแบบสอบถามบัญชีรายการพร้อมกับ limit และ offset.

ฟังก์ชันนี้ส่งคืนวัตถุแถว มันทำงานเหมือนตัว วนซ้ำ ซึ่งช่วยให้เราสามารถเรียกใช้ผ่านระเบียนทีละรายการ และสแกนแต่ละระเบียนลงในวัตถุบัญชีและผนวกเข้ากับรายการ

ในที่สุดก็ปิดแถวเพื่อหลีกเลี่ยงการเชื่อมต่อ db leaking นอกจากนี้ยังตรวจสอบว่ามีข้อผิดพลาดใด ๆ หรือไม่ก่อนที่จะส่งคืน items ให้กับผู้เรียก

โค้ดดูค่อนข้างยาว แต่เข้าใจง่าย สรุปคือ มันวิ่งเร็วมาก! และเราไม่ต้องกังวลกับการทำผิดพลาดโง่ ๆ ในโค้ดเพราะ sqlc รับรองแล้วว่าโค้ดที่สร้างขึ้นจะทำงานได้อย่างสมบูรณ์


UPDATE การดำเนินการ


ตกลง ไปที่การดำเนินการถัดไป: UPDATE.


มาคัดลอกโค้ดนี้ไปยังไฟล์ account.sql ของเราและเปลี่ยนชื่อฟังก์ชันเป็น UpdateAccount()

-- name: UpdateAccount :exec
UPDATE accounts
SET balance = $2
WHERE id = $1;

ที่นี่เราใช้ป้ายกำกับใหม่ :exec เนื่องจากคำสั่งนี้ไม่ส่งคืนข้อมูลใด ๆ เพียงอัปเดต 1 แถวในฐานข้อมูล

สมมติว่าเราอนุญาตให้อัปเดตบัญชี balance เท่านั้น ส่วนบัญชี owner และ currency ไม่ควรเปลี่ยน

เราใช้อนุประโยค WHERE เพื่อระบุ id บัญชีที่เราต้องการอัปเดต


สร้างโค้ด Go เพื่ออัปเดตบัญชี


ตอนนี้รัน make sqlc ในเทอร์มินัลเพื่อสร้างรหัสใหม่

make sqlc

และแน่นอน เรามีฟังก์ชัน UpdateAccount()

const updateAccount = `-- name: UpdateAccount :exec
UPDATE accounts
SET balance = $2
WHERE id = $1
`

type UpdateAccountParams struct {
	ID      int64 `json:"id"`
	Balance int64 `json:"balance"`
}

func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) error {
	_, err := q.db.ExecContext(ctx, updateAccount, arg.ID, arg.Balance)
	return err
}

ต้องใช้บริบท บัญชี id และ พารามิเตอร์ balance เป็นข้อมูลเข้า ทั้งหมดที่ทำได้คือการเรียก ExecContext()โดยใช้การสืบค้นและป้อนอาร์กิวเมนต์ จากนั้นจึงส่งกลับ error ไปยังผู้เรียก

ส่งคืนแถวที่อัปเดต


บางครั้ง การส่งคืนอ็อบเจ็กต์บัญชีที่อัปเดตด้วยก็มีประโยชน์เช่นกัน ในกรณีนั้น เราสามารถเปลี่ยนป้ายกำกับ :exec เป็น :one และเพิ่ม RETURNING * ที่ส่วนท้ายของคิวรีอัปเดตนี้:

-- name: UpdateAccount :one
UPDATE accounts
SET balance = $2
WHERE id = $1
RETURNING *;

จากนั้นสร้างโค้ดใหม่

type UpdateAccountParams struct {
	ID      int64 `json:"id"`
	Balance int64 `json:"balance"`
}

func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
	row := q.db.QueryRowContext(ctx, updateAccount, arg.ID, arg.Balance)
	var i Account
	err := row.Scan(
		&i.ID,
		&i.Owner,
		&i.Balance,
		&i.Currency,
		&i.CreatedAt,
	)
	return i, err
}

ขณะนี้คิวรี SQL ได้เปลี่ยนไปแล้ว และ ฟังก์ชัน UpdateAccount() จะส่งคืนการอัปเดต Accountพ ร้อมกับส่วนขยาย error


การดำเนินการลบ


ปฏิบัติการสุดท้ายคือ DELETE. ง่ายกว่าการอัพเดทด้วยซ้ำ

เขียนแบบสอบถาม SQL เพื่อลบบัญชี

มาคัดลอกแบบสอบถามตัวอย่างนี้และเปลี่ยนชื่อฟังก์ชันเป็น DeleteAccount. ฉันไม่ต้องการ postgres เพื่อส่งคืนบันทึกที่ถูกลบ ลองใช้:execlabel กัน เถอะ

-- name: DeleteAccount :exec
DELETE FROM accounts
WHERE id = $1;

สร้างโค้ด Go เพื่อลบบัญชี
มาเรียกใช้ make sqlc เพื่อสร้างโค้ดใหม่ และตอนนี้เรามีฟังก์ชัน DeleteAccount() ในโค้ดแล้ว

const deleteAccount = `-- name: DeleteAccount :exec
DELETE FROM accounts
WHERE id = $1
`

func (q *Queries) DeleteAccount(ctx context.Context, id int64) error {
	_, err := q.db.ExecContext(ctx, deleteAccount, id)
	return err
}

โดยพื้นฐานแล้ว เราได้เรียนรู้วิธีสร้างการดำเนินการ CRUD แบบเต็มสำหรับaccountsตาราง ของเรา

credit : https://dev.to/techschoolguru/

Leave a Reply

Your email address will not be published. Required fields are marked *