REST API با Go، Chi، MySQL و sqlx

این ادامه پست قبلی REST API با Go، Chi و InMemory Store است. در این آموزش من سرویس را برای ذخیره داده ها در پایگاه داده MySQL گسترش خواهم داد. من از Docker برای اجرای MySQL و اجرای مهاجرت پایگاه داده استفاده خواهم کرد.
راه اندازی پروژه
من با کپی کردن مطالب شروع می کنم https://github.com/kashifsoofi/blog-code-samples/tree/main/movies-api-with-go-chi-and-memory-store
، آن را در یک پوشه جدید قرار دهید movies-api-with-go-chi-and-mysql
و به روز رسانی نام ماژول در go.mod
برای مطابقت با پوشه جدید و به روز رسانی در فایل های منبع که در آن استفاده می شود. این معمولاً ریشه شما خواهد بود git
مخزن است و به این شرح مفصل نخواهد بود.
راه اندازی سرور پایگاه داده
من از یک docker-compose برای اجرای MySQL در یک کانتینر docker استفاده خواهم کرد. این به ما امکان می دهد خدمات بیشتری را اضافه کنیم که api استراحت ما به عنوان مثال سرور redis برای کش توزیع شده به آن وابسته است.
بیایید با افزودن یک فایل جدید به نام به عنوان شروع کنیم docker-compose.dev-env.yml
، با خیال راحت آن را هر طور که دوست دارید نام ببرید. برای افزودن یک نمونه پایگاه داده برای api استراحت فیلم، محتوای زیر را اضافه کنید.
version: '3.7'
services:
movies.db:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=Password123
- MYSQL_DATABASE=moviesdb
volumes:
- moviesdbdata:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: "mysql -uroot -pPassword123 moviesdb -e 'select 1'"
timeout: 20s
interval: 10s
retries: 10
volumes:
moviesdbdata:
یک ترمینال در ریشه راه حل که فایل docker-compose در آن قرار دارد باز کنید و دستور زیر را برای راه اندازی سرور پایگاه داده اجرا کنید.
docker-compose -f docker-compose.dev-env.yml up -d
مهاجرت های پایگاه داده
قبل از اینکه بتوانیم استفاده از MySQL را شروع کنیم، باید یک جدول برای ذخیره داده های خود ایجاد کنیم. من از ابزار مهاجرت پایگاه داده عالی استفاده خواهم کرد، همچنین می توان آن را به عنوان کتابخانه وارد کرد.
برای مهاجرت من یک پوشه ایجاد کرده ام db
و یک پوشه دیگر به نام migrations
زیر db من دستورات زیر را برای ایجاد مهاجرت اجرا کردم.
migrate create -ext sql -dir db/migrations -seq schema_movies_create
migrate create -ext sql -dir db/migrations -seq table_movies_create
با این کار 4 فایل ایجاد می شود که برای هر مهاجرت یک فایل وجود دارد up
و الف down
فیلمنامه، up
هنگام اعمال مهاجرت و اجرا خواهد شد down
زمانی که تغییر را به عقب برگردانید اجرا می شود.
-
000001_schema_movies_create.up.sql
CREATE SCHEMA IF NOT EXISTS Movies;
-
000001_schema_movies_create.down.sql
DROP SCHEMA IF EXISTS Movies;
-
000002_table_movies_create.up.sql
CREATE TABLE IF NOT EXISTS Movies (
Id CHAR(36) NOT NULL UNIQUE,
Title VARCHAR(100) NOT NULL,
Director VARCHAR(100) NOT NULL,
ReleaseDate DATETIME NOT NULL,
TicketPrice DECIMAL(12, 4) NOT NULL,
CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL,
PRIMARY KEY (Id)
) ENGINE=INNODB;
-
000002_table_movies_create.down.sql
DROP TABLE IF EXISTS Movies;
من معمولاً کانتینری ایجاد میکنم که همه مهاجرتهای پایگاه داده و ابزاری برای اجرای آن مهاجرتها داشته باشد. Dockerfile
برای اجرای مهاجرت های پایگاه داده به صورت زیر است
FROM migrate/migrate
# Copy all db files
COPY ./migrations /migrations
ENTRYPOINT [ "migrate", "-path", "/migrations", "-database"]
CMD ["mysql://root:Password123@tcp(movies.db:3306)/moviesdb up"]
موارد زیر را اضافه کنید docker-compose.dev-env.yml
فایل برای اضافه کردن محفظه مهاجرت و اجرای مهاجرت در هنگام راه اندازی. لطفاً به یاد داشته باشید که اگر مهاجرتهای جدیدی اضافه کنید، باید کانتینر و را حذف کنید movies.db.migrations
تصویر برای افزودن فایل های مهاجرتی جدید به تصویر.
movies.db.migrations:
depends_on:
movies.db:
condition: service_healthy
image: movies.db.migrations
build:
context: ./db/
dockerfile: Dockerfile
command: "'mysql://root:Password123@tcp(movies.db:3306)/moviesdb' up"
یک ترمینال را در ریشه پروژه باز کنید که در آن فایل docker-compose مکان است و دستور زیر را برای راه اندازی سرور پایگاه داده و اعمال مهاجرت برای ایجاد اجرا کنید. Movies
طرحواره و Movies
جدول.
docker-compose -f docker-compose.dev-env.yml up -d
فروشگاه فیلم MySQL
من از sqlx برای اجرای پرس و جوها و ستون های نقشه برای ساخت فیلدها و بالعکس استفاده خواهم کرد. sqlx
کتابخانه ای است که مجموعه ای از برنامه های افزودنی را به صورت استاندارد ارائه می دهد database/sql
کتابخانه
یک فایل جدید به نام اضافه کنید mysql_movies_store.go
. یک ساختار جدید اضافه کنید MySqlMoviesStore
حاوی databaseUrl
و اشاره گر به sqlx.DB
، همچنین روش های کمکی را به آن اضافه کنید connect
به پایگاه داده و close
اتصال نیز. همچنین توجه داشته باشید که من یک را اضافه کرده ام noOpMapper
روش و به عنوان MapperFunc از تنظیم کنید sqlx.DB
، دلیل این امر استفاده از پوشش مشابه نام فیلد struct است. رفتار پیش فرض برای sqlx
نگاشت نام فیلدها به نام ستون ها با حروف کوچک است.
package store
import (
"context"
"database/sql"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
const driverName = "mysql"
type MySqlMoviesStore struct {
databaseUrl string
dbx *sqlx.DB
}
func NewMySqlMoviesStore(databaseUrl string) *MySqlMoviesStore {
return &MySqlMoviesStore{
databaseUrl: databaseUrl,
}
}
func noOpMapper(s string) string { return s }
func (s *MySqlMoviesStore) connect(ctx context.Context) error {
dbx, err := sqlx.ConnectContext(ctx, driverName, s.databaseUrl)
if err != nil {
return err
}
dbx.MapperFunc(noOpMapper)
s.dbx = dbx
return nil
}
func (s *MySqlMoviesStore) close() error {
return s.dbx.Close()
}
تگ db را اضافه کنید
به روز رسانی Movie
ساختن در movies_store.go
فایل برای اضافه کردن برچسب db برای ID
فیلد، این به sqlx اجازه می دهد تا نقشه برداری کند ID
فیلد برای تصحیح ستون جایگزین برای این استفاده از AS
در پرس و جوهای انتخاب کنید یا نام ستون را در پایگاه داده به عنوان تغییر نام دهید ID
. با استفاده از تمام فیلدهای دیگر به درستی نگاشت می شوند noOpMapper
از قسمت بالا
type Movie struct {
ID uuid.UUID `db:"Id"`
...
}
متن نوشته
ما از آن استفاده نکردیم Context
در نمونه قبلی movies-api-with-go-chi-and-memory-store
، اکنون که در حال اتصال به یک حافظه خارجی و بسته هستیم، می خواهیم از روش های پشتیبانی پرس و جو برای پذیرش استفاده کنیم. Context
ما خود را به روز خواهیم کرد store.Interface
پذیرفتن Context
و هنگام اجرای پرس و جو از آن استفاده کنید. store.Interface
به شرح زیر به روز خواهد شد
type Interface interface {
GetAll(ctx context.Context) ([]Movie, error)
GetByID(ctx context.Context, id uuid.UUID) (Movie, error)
Create(ctx context.Context, createMovieParams CreateMovieParams) error
Update(ctx context.Context, id uuid.UUID, updateMovieParams UpdateMovieParams) error
Delete(ctx context.Context, id uuid.UUID) error
}
ما همچنین باید به روز رسانی کنیم MemoryMoviesStore
روش های پذیرش Context
برای ارضای store.Interface
و روش های به روز رسانی در movies_handler
برای عبور متن درخواست با استفاده از r.Context()
هنگام تماس store
مواد و روش ها.
ايجاد كردن
ما با استفاده از پایگاه داده متصل می شویم connect
روش کمکی، یک نمونه جدید از Movie
و insert query را با NamedExecContext
. ما در حال رسیدگی به یک error
و برگشت DuplicateKeyError
اگر خطای برگشتی حاوی متن باشد Error 1062
. اگر درج موفقیت آمیز بود، ما برمی گردیم nil
.
ایجاد تابع به نظر می رسد
func (s *MySqlMoviesStore) Create(ctx context.Context, createMovieParams CreateMovieParams) error {
err := s.connect(ctx)
if err != nil {
return err
}
defer s.close()
movie := Movie{
ID: createMovieParams.ID,
Title: createMovieParams.Title,
Director: createMovieParams.Director,
ReleaseDate: createMovieParams.ReleaseDate,
TicketPrice: createMovieParams.TicketPrice,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if _, err := s.dbx.NamedExecContext(
ctx,
`INSERT INTO Movies
(Id, Title, Director, ReleaseDate, TicketPrice, CreatedAt, UpdatedAt)
VALUES
(:Id, :Title, :Director, :ReleaseDate, :TicketPrice, :CreatedAt, :UpdatedAt)`,
movie); err != nil {
if strings.Contains(err.Error(), "Error 1062") {
return &DuplicateKeyError{ID: createMovieParams.ID}
}
return err
}
return nil
}
GetAll
ما با استفاده از پایگاه داده متصل می شویم connect
روش کمکی، سپس استفاده کنید SelectContext
روش از sqlx
برای اجرای پرس و جو، sqlx
ستون ها را به فیلدها نگاشت می کرد. اگر پرس و جو موفقیت آمیز باشد، برشی از فیلم های بارگذاری شده را برمی گردانیم.
func (s *MySqlMoviesStore) GetAll(ctx context.Context) ([]Movie, error) {
err := s.connect(ctx)
if err != nil {
return nil, err
}
defer s.close()
var movies []Movie
if err := s.dbx.SelectContext(
ctx,
&movies,
`SELECT
Id, Title, Director, ReleaseDate, TicketPrice, CreatedAt, UpdatedAt
FROM Movies`); err != nil {
return nil, err
}
return movies, nil
}
اگر در تجزیه خطایی وجود دارد DATETIME
ستون، به یاد داشته باشید که اضافه کنید parseTime=true
پارامتر رشته اتصال شما
GetByID
ما با استفاده از پایگاه داده متصل می شویم connect
روش کمکی، سپس استفاده کنید GetContext
روش اجرای پرس و جو انتخاب، sqlx
ستون ها را به فیلدها نگاشت می کرد. اگر راننده برگردد sql.ErrNoRows
سپس برمی گردیم store.RecordNotFoundError
. اگر با موفقیت بارگذاری شد movie
رکورد برگردانده می شود.
func (s *MySqlMoviesStore) GetByID(ctx context.Context, id uuid.UUID) (Movie, error) {
err := s.connect(ctx)
if err != nil {
return Movie{}, err
}
defer s.close()
var movie Movie
if err := s.dbx.GetContext(
ctx,
&movie,
`SELECT
Id, Title, Director, ReleaseDate, TicketPrice, CreatedAt, UpdatedAt
FROM Movies
WHERE Id = ?`,
id); err != nil {
if err != sql.ErrNoRows {
return Movie{}, err
}
return Movie{}, &RecordNotFoundError{}
}
return movie, nil
}
به روز رسانی
ما با استفاده از پایگاه داده متصل می شویم connect
روش کمکی، سپس استفاده کنید NamedExecContext
روشی برای اجرای پرس و جو برای به روز رسانی یک رکورد موجود.
func (s *MySqlMoviesStore) Update(ctx context.Context, id uuid.UUID, updateMovieParams UpdateMovieParams) error {
err := s.connect(ctx)
if err != nil {
return err
}
defer s.close()
movie := Movie{
ID: id,
Title: updateMovieParams.Title,
Director: updateMovieParams.Director,
ReleaseDate: updateMovieParams.ReleaseDate,
TicketPrice: updateMovieParams.TicketPrice,
UpdatedAt: time.Now().UTC(),
}
if _, err := s.dbx.NamedExecContext(
ctx,
`UPDATE Movies
SET Title = :Title, Director = :Director, ReleaseDate = :ReleaseDate, TicketPrice = :TicketPrice, UpdatedAt = :UpdatedAt
WHERE Id = :Id`,
movie); err != nil {
return err
}
return nil
}
حذف
ما با استفاده از پایگاه داده متصل می شویم connect
روش helper، سپس query را برای حذف یک رکورد موجود با استفاده از آن اجرا کنید ExecContext
.
func (s *MySqlMoviesStore) Delete(ctx context.Context, id uuid.UUID) error {
err := s.connect(ctx)
if err != nil {
return err
}
defer s.close()
if _, err := s.dbx.ExecContext(
ctx,
`DELETE FROM Movies
WHERE id = ?`, id); err != nil {
return err
}
return nil
}
پیکربندی پایگاه داده
یک ساختار جدید به نام اضافه کنید Database
که در config.go
و آن را به Configuration
ساختار نیز.
type Configuration struct {
HTTPServer
Database
}
...
type Database struct {
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
LogLevel string `envconfig:"DATABASE_LOG_LEVEL" default:"warn"`
MaxOpenConnections int `envconfig:"DATABASE_MAX_OPEN_CONNECTIONS" default:"10"`
}
تزریق وابستگی
به روز رسانی main.go
به صورت زیر برای ایجاد یک نمونه جدید از MySqlMoviesStore
، من تصمیم به ایجاد نمونه ای از MySqlMoviesStore
بجای MemoryMoviesStore
، راه حل را می توان برای ایجاد یکی از وابستگی ها بر اساس یک پیکربندی تقویت کرد.
// store := store.NewMemoryMoviesStore()
store := store.NewMySqlMoviesStore(cfg.DatabaseURL)
تست
من هیچ واحد یا تست ادغام را برای این آموزش اضافه نمی کنم، شاید یک آموزش زیر. اما تمام نقاط پایانی را می توان با استفاده از Postman با پیروی از طرح آزمایشی مقاله قبلی آزمایش کرد.
شما می توانید rest api را با اجرای mysql در docker با اجرای زیر شروع کنید
DATABASE_URL=root:Password123@tcp(localhost:3306)/moviesdb?parseTime=true go run main.go
منبع
کد منبع برای برنامه آزمایشی در GitHub در مخزن نمونه کدهای وبلاگ میزبانی می شود.
منابع
بدون ترتیب خاصی