برنامه نویسی

برای دستیابی به عملیات تراکنش راحت‌تر، Sqlc را کپسوله می‌کند

Summarize this content to 400 words in Persian Lang

SQLC چیست؟

SQLC یک ابزار توسعه قدرتمند است که وظیفه اصلی آن تبدیل پرس و جوهای SQL به کد Go-safe نوع است. با تجزیه عبارات SQL و تجزیه و تحلیل ساختارهای پایگاه داده، sqlc می تواند به طور خودکار ساختارها و توابع Go متناظر را تولید کند و فرآیند نوشتن کد برای عملیات پایگاه داده را بسیار ساده کند.

با استفاده از sqlc، توسعه دهندگان می توانند روی نوشتن پرس و جوهای SQL تمرکز کنند و کار خسته کننده تولید کد Go را به ابزار بسپارند، بنابراین روند توسعه را تسریع کرده و کیفیت کد را بهبود می بخشند.

اجرای تراکنش SQLC

کد تولید شده توسط Sqlc معمولا حاوی یک ساختار Queries است که تمام عملیات پایگاه داده را در بر می گیرد. این ساختار یک رابط کلی Querier را پیاده سازی می کند که تمام روش های جستجوی پایگاه داده را تعریف می کند.

نکته کلیدی این است که تابع New تولید شده توسط sqlc می تواند هر شیئی را که رابط DBTX را پیاده سازی می کند، از جمله *sql.DB و *sql.Tx بپذیرد.

هسته اجرای تراکنش استفاده از چند شکلی رابط Go است. هنگامی که نیاز به انجام عملیات در یک تراکنش دارید، یک شی *sql.Tx ایجاد می کنید و سپس آن را به تابع New ارسال می کنید تا یک نمونه Queries جدید ایجاد کنید. این نمونه تمام عملیات را در چارچوب یک تراکنش انجام می دهد.

فرض کنید از طریق pgx به پایگاه داده Postgres متصل شده و Queries را با کد زیر مقداردهی اولیه می کنیم.

var Pool *pgxpool.Pool
var Queries *sqlc.Queries

func init() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

connConfig, err := pgxpool.ParseConfig(“postgres://user:password@127.0.0.1:5432/db?sslmode=disable”)
if err != nil {
panic(err)
}

pool, err := pgxpool.NewWithConfig(ctx, connConfig)
if err != nil {
panic(err)
}
if err := pool.Ping(ctx); err != nil {
panic(err)
}

Pool = pool
Queries = sqlc.New(pool)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کپسوله کردن معاملات

کد زیر یک کپسوله سازی هوشمندانه تراکنش sqlc است که فرآیند استفاده از تراکنش های پایگاه داده در Go را ساده می کند. این تابع یک زمینه و یک تابع فراخوانی را به عنوان پارامتر می پذیرد.

func WithTransaction(ctx context.Context, callback func(qtx *sqlc.Queries) (err error)) (err error) {
tx, err := Pool.Begin(ctx)
if err != nil {
return err
}
defer func() {
if e := tx.Rollback(ctx); e != nil && !errors.Is(e, pgx.ErrTxClosed) {
err = e
}
}()

if err := callback(Queries.WithTx(tx)); err != nil {
return err
}

return tx.Commit(ctx)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این تابع ابتدا یک تراکنش جدید را شروع می کند و سپس اجرا را به تأخیر می اندازد تا اطمینان حاصل شود که تراکنش در نهایت بازگردانده می شود مگر اینکه به صراحت متعهد شده باشد. این یک مکانیسم ایمنی برای جلوگیری از اشغال منابع تراکنش های ناتمام است. در مرحله بعد، تابع فراخوانی ارائه شده توسط کاربر را فراخوانی می‌کند و در یک شی پرس و جو با زمینه تراکنش ارسال می‌کند و به کاربر اجازه می‌دهد تا عملیات پایگاه داده مورد نیاز را در تراکنش انجام دهد.

اگر فراخوانی با موفقیت بدون خطا اجرا شود، تابع تراکنش را انجام می دهد. هر گونه خطایی که در طول فرآیند رخ دهد باعث می شود تراکنش به عقب بازگردد. این روش نه تنها ثبات داده ها را تضمین می کند، بلکه مدیریت خطا را نیز بسیار ساده می کند.

ظرافت این کپسوله سازی در این است که منطق مدیریت تراکنش پیچیده را پشت یک فراخوانی تابع ساده پنهان می کند. کاربران می‌توانند بدون نگرانی در مورد شروع، انجام یا لغو تراکنش‌ها، روی نوشتن منطق تجاری تمرکز کنند.

استفاده از این کد کاملاً شهودی است. می‌توانید تابع db.WithTransaction را در جایی که نیاز به انجام یک تراکنش دارید، فراخوانی کنید و تابعی را به عنوان پارامتری ارسال کنید که تمام عملیات پایگاه داده‌ای را که می‌خواهید در تراکنش انجام دهید، تعریف می‌کند.

err := db.WithTransaction(ctx, func(qtx *sqlc.Queries) error {
// 在这里执行你的数据库操作
// 例如:
_, err := qtx.CreateUser(ctx, sqlc.CreateUserParams{
Name: “Alice”,
Email: “alice@example.com”,
})
if err != nil {
return err
}

_, err = qtx.CreatePost(ctx, sqlc.CreatePostParams{
Title: “First Post”,
Content: “Hello, World!”,
AuthorID: newUserID,
})
if err != nil {
return err
}

// 如果所有操作都成功,返回 nil
return nil
})

if err != nil {
// 处理错误
log.Printf(“transaction failed: %v”, err)
} else {
log.Println(“transaction completed successfully”)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مثال، یک کاربر و یک پست در یک تراکنش ایجاد می کنیم. اگر هر عملیاتی با شکست مواجه شود، کل تراکنش برگردانده می شود. اگر همه عملیات موفقیت آمیز باشد، تراکنش متعهد می شود.

مزیت این رویکرد این است که شما نیازی به مدیریت دستی شروع، تعهد یا بازگشت تراکنش ندارید که همه اینها توسط تابع db.WithTransaction مدیریت می شود. شما فقط باید بر روی عملیات واقعی پایگاه داده انجام شده در تراکنش تمرکز کنید. این کار کد را تا حد زیادی ساده می کند و احتمال خطا را کاهش می دهد.

بسته بندی بیشتر

روش بسته بندی که در بالا ذکر شد بدون معایب نیست.

این کپسوله‌سازی ساده تراکنش در هنگام برخورد با تراکنش‌های تودرتو محدودیت‌هایی دارد. این به این دلیل است که هر بار به جای اینکه بررسی کند آیا قبلاً در یک تراکنش هستید یا خیر، یک تراکنش جدید ایجاد می کند.

برای اجرای پردازش تراکنش تودرتو، باید شی تراکنش فعلی را بدست آوریم، اما شی تراکنش فعلی در داخل sqlc.Queries پنهان است، بنابراین باید sqlc.Queries را گسترش دهیم.

ساختار گسترش دهنده sqlc.Queries توسط ما به عنوان Repositories ایجاد شده است که *sqlc.Queries را گسترش می دهد و یک pool ویژگی جدید اضافه می کند که یک اشاره گر از نوع pgxpool.Pool است.

type Repositories struct {
*sqlc.Queries
pool *pgxpool.Pool
}

func NewRepositories(pool *pgxpool.Pool) *Repositories {
return &Repositories{
pool: pool,
Queries: sqlc.New(pool),
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اما هنگامی که ما شروع به نوشتن کد می کنیم، متوجه می شویم که *pgxpool.Pool نمی تواند رابط pgx.Tx را برآورده کند زیرا *pgxpool فاقد متدهای Rollback و Commit است برای حل این مشکل، ما به گسترش Repositories ادامه می دهیم، یک ویژگی جدید tx به آن اضافه می کنیم و یک متد NewRepositoriesTx جدید به آن اضافه می کنیم.

type Repositories struct {
*sqlc.Queries
tx pgx.Tx
pool *pgxpool.Pool
}

func NewRepositoriesTx(tx pgx.Tx) *Repositories {
return &Repositories{
tx: tx,
Queries: sqlc.New(tx),
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون، ساختار مخازن ما دارای ویژگی‌های pool و tx است تراکنش ها هیچ راهی برای پایان دادن به تراکنش وجود ندارد و یکی از راه های حل این مشکل ایجاد ساختار RepositoriesTX دیگر و ذخیره pgx.Tx در آن به جای *pgxpool.Pool است، اما انجام این کار ممکن است جدید را به همراه داشته باشد. ممکن است مجبور باشیم روش WithTransaction را به ترتیب برای هر دوی آنها پیاده سازی کنیم.

func (r *Repositories) WithTransaction(ctx context.Context, fn func(qtx *Repositories) (err error)) (err error) {
var tx pgx.Tx
if r.tx != nil {
tx, err = r.tx.Begin(ctx)
} else {
tx, err = r.pool.Begin(ctx)
}
if err != nil {
return err
}
defer func() {
if e := tx.Rollback(ctx); e != nil && !errors.Is(e, pgx.ErrTxClosed) {
err = e
}
}()

if err := fn(NewRepositoriesTx(tx)); err != nil {
return err
}

return tx.Commit(ctx)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تفاوت اصلی این روش با WithTransaction پیاده‌سازی شده در فصل قبل این است که به جای جهانی روی *Repositories پیاده‌سازی می‌شود، بنابراین می‌توانیم تراکنش‌های تودرتو را از طریق pgx.Tx در (r *Repositories) شروع کنیم.

هنگامی که یک تراکنش شروع نشده است، می‌توانیم Repositories.WithTransaction را برای شروع یک تراکنش جدید فراخوانی کنیم.

err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error {

return nil
})

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تراکنش های چند سطحی نیز مشکلی ندارند و پیاده سازی آن بسیار آسان است.

err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error {
// 假设此处进行了一些数据操作
// 然后,开启一个嵌套事务
return tx.WithTransaction(ctx, func(tx *db.Repositories) error {
// 这里可以在嵌套事务中进行一些操作
return nil
})
})

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این طرح کپسوله‌سازی به طور موثر اتمی بودن عملیات را تضمین می‌کند، حتی اگر هر یک از عملیات‌ها با شکست مواجه شود، کل تراکنش به عقب بازگردانده می‌شود و در نتیجه ثبات داده‌ها را تضمین می‌کند.

نتیجه

این مقاله راه حلی را برای کپسوله کردن تراکنش های پایگاه داده SQLC با استفاده از Go و کتابخانه pgx معرفی می کند.

هسته ساختار Repositories است که رابط پرس و جو SQLC و منطق پردازش تراکنش را در بر می گیرد. با متد WithTransaction، می‌توانیم یک تراکنش فرعی جدید را روی یک تراکنش موجود شروع کنیم یا یک تراکنش جدید را در استخر اتصال شروع کنیم و اطمینان حاصل کنیم که تراکنش پس از بازگشت عملکرد برگشت داده می‌شود.

سازنده NewRepositories و NewRepositoriesTx به ترتیب برای ایجاد نمونه های مخازن معمولی و درون تراکنش استفاده می شود.

این می تواند چندین عملیات پایگاه داده را در یک تراکنش محصور کند، اگر هر عملیاتی با شکست مواجه شود، تراکنش برگردانده می شود و قابلیت نگهداری و خوانایی کد را بهبود می بخشد.

SQLC چیست؟

SQLC یک ابزار توسعه قدرتمند است که وظیفه اصلی آن تبدیل پرس و جوهای SQL به کد Go-safe نوع است. با تجزیه عبارات SQL و تجزیه و تحلیل ساختارهای پایگاه داده، sqlc می تواند به طور خودکار ساختارها و توابع Go متناظر را تولید کند و فرآیند نوشتن کد برای عملیات پایگاه داده را بسیار ساده کند.

با استفاده از sqlc، توسعه دهندگان می توانند روی نوشتن پرس و جوهای SQL تمرکز کنند و کار خسته کننده تولید کد Go را به ابزار بسپارند، بنابراین روند توسعه را تسریع کرده و کیفیت کد را بهبود می بخشند.

اجرای تراکنش SQLC

کد تولید شده توسط Sqlc معمولا حاوی یک ساختار Queries است که تمام عملیات پایگاه داده را در بر می گیرد. این ساختار یک رابط کلی Querier را پیاده سازی می کند که تمام روش های جستجوی پایگاه داده را تعریف می کند.

نکته کلیدی این است که تابع New تولید شده توسط sqlc می تواند هر شیئی را که رابط DBTX را پیاده سازی می کند، از جمله *sql.DB و *sql.Tx بپذیرد.

هسته اجرای تراکنش استفاده از چند شکلی رابط Go است. هنگامی که نیاز به انجام عملیات در یک تراکنش دارید، یک شی *sql.Tx ایجاد می کنید و سپس آن را به تابع New ارسال می کنید تا یک نمونه Queries جدید ایجاد کنید. این نمونه تمام عملیات را در چارچوب یک تراکنش انجام می دهد.

فرض کنید از طریق pgx به پایگاه داده Postgres متصل شده و Queries را با کد زیر مقداردهی اولیه می کنیم.

var Pool *pgxpool.Pool
var Queries *sqlc.Queries

func init() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    defer cancel()

    connConfig, err := pgxpool.ParseConfig("postgres://user:password@127.0.0.1:5432/db?sslmode=disable")
    if err != nil {
        panic(err)
    }

    pool, err := pgxpool.NewWithConfig(ctx, connConfig)
    if err != nil {
        panic(err)
    }
    if err := pool.Ping(ctx); err != nil {
        panic(err)
    }

    Pool = pool
    Queries = sqlc.New(pool)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کپسوله کردن معاملات

کد زیر یک کپسوله سازی هوشمندانه تراکنش sqlc است که فرآیند استفاده از تراکنش های پایگاه داده در Go را ساده می کند. این تابع یک زمینه و یک تابع فراخوانی را به عنوان پارامتر می پذیرد.

func WithTransaction(ctx context.Context, callback func(qtx *sqlc.Queries) (err error)) (err error) {
    tx, err := Pool.Begin(ctx)
    if err != nil {
        return err
    }
    defer func() {
        if e := tx.Rollback(ctx); e != nil && !errors.Is(e, pgx.ErrTxClosed) {
            err = e
        }
    }()

    if err := callback(Queries.WithTx(tx)); err != nil {
        return err
    }

    return tx.Commit(ctx)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این تابع ابتدا یک تراکنش جدید را شروع می کند و سپس اجرا را به تأخیر می اندازد تا اطمینان حاصل شود که تراکنش در نهایت بازگردانده می شود مگر اینکه به صراحت متعهد شده باشد. این یک مکانیسم ایمنی برای جلوگیری از اشغال منابع تراکنش های ناتمام است. در مرحله بعد، تابع فراخوانی ارائه شده توسط کاربر را فراخوانی می‌کند و در یک شی پرس و جو با زمینه تراکنش ارسال می‌کند و به کاربر اجازه می‌دهد تا عملیات پایگاه داده مورد نیاز را در تراکنش انجام دهد.

اگر فراخوانی با موفقیت بدون خطا اجرا شود، تابع تراکنش را انجام می دهد. هر گونه خطایی که در طول فرآیند رخ دهد باعث می شود تراکنش به عقب بازگردد. این روش نه تنها ثبات داده ها را تضمین می کند، بلکه مدیریت خطا را نیز بسیار ساده می کند.

ظرافت این کپسوله سازی در این است که منطق مدیریت تراکنش پیچیده را پشت یک فراخوانی تابع ساده پنهان می کند. کاربران می‌توانند بدون نگرانی در مورد شروع، انجام یا لغو تراکنش‌ها، روی نوشتن منطق تجاری تمرکز کنند.

استفاده از این کد کاملاً شهودی است. می‌توانید تابع db.WithTransaction را در جایی که نیاز به انجام یک تراکنش دارید، فراخوانی کنید و تابعی را به عنوان پارامتری ارسال کنید که تمام عملیات پایگاه داده‌ای را که می‌خواهید در تراکنش انجام دهید، تعریف می‌کند.

err := db.WithTransaction(ctx, func(qtx *sqlc.Queries) error {
    // 在这里执行你的数据库操作
    // 例如:
    _, err := qtx.CreateUser(ctx, sqlc.CreateUserParams{
        Name: "Alice",
        Email: "alice@example.com",
    })
    if err != nil {
        return err
    }

    _, err = qtx.CreatePost(ctx, sqlc.CreatePostParams{
        Title: "First Post",
        Content: "Hello, World!",
        AuthorID: newUserID,
    })
    if err != nil {
        return err
    }

    // 如果所有操作都成功,返回 nil
    return nil
})

if err != nil {
    // 处理错误
    log.Printf("transaction failed: %v", err)
} else {
    log.Println("transaction completed successfully")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مثال، یک کاربر و یک پست در یک تراکنش ایجاد می کنیم. اگر هر عملیاتی با شکست مواجه شود، کل تراکنش برگردانده می شود. اگر همه عملیات موفقیت آمیز باشد، تراکنش متعهد می شود.

مزیت این رویکرد این است که شما نیازی به مدیریت دستی شروع، تعهد یا بازگشت تراکنش ندارید که همه اینها توسط تابع db.WithTransaction مدیریت می شود. شما فقط باید بر روی عملیات واقعی پایگاه داده انجام شده در تراکنش تمرکز کنید. این کار کد را تا حد زیادی ساده می کند و احتمال خطا را کاهش می دهد.

بسته بندی بیشتر

روش بسته بندی که در بالا ذکر شد بدون معایب نیست.

این کپسوله‌سازی ساده تراکنش در هنگام برخورد با تراکنش‌های تودرتو محدودیت‌هایی دارد. این به این دلیل است که هر بار به جای اینکه بررسی کند آیا قبلاً در یک تراکنش هستید یا خیر، یک تراکنش جدید ایجاد می کند.

برای اجرای پردازش تراکنش تودرتو، باید شی تراکنش فعلی را بدست آوریم، اما شی تراکنش فعلی در داخل sqlc.Queries پنهان است، بنابراین باید sqlc.Queries را گسترش دهیم.

ساختار گسترش دهنده sqlc.Queries توسط ما به عنوان Repositories ایجاد شده است که *sqlc.Queries را گسترش می دهد و یک pool ویژگی جدید اضافه می کند که یک اشاره گر از نوع pgxpool.Pool است.

type Repositories struct {
    *sqlc.Queries
    pool *pgxpool.Pool
}

func NewRepositories(pool *pgxpool.Pool) *Repositories {
    return &Repositories{
        pool:    pool,
        Queries: sqlc.New(pool),
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اما هنگامی که ما شروع به نوشتن کد می کنیم، متوجه می شویم که *pgxpool.Pool نمی تواند رابط pgx.Tx را برآورده کند زیرا *pgxpool فاقد متدهای Rollback و Commit است برای حل این مشکل، ما به گسترش Repositories ادامه می دهیم، یک ویژگی جدید tx به آن اضافه می کنیم و یک متد NewRepositoriesTx جدید به آن اضافه می کنیم.

type Repositories struct {
    *sqlc.Queries
    tx   pgx.Tx
    pool *pgxpool.Pool
}

func NewRepositoriesTx(tx pgx.Tx) *Repositories {
    return &Repositories{
        tx:      tx,
        Queries: sqlc.New(tx),
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون، ساختار مخازن ما دارای ویژگی‌های pool و tx است تراکنش ها هیچ راهی برای پایان دادن به تراکنش وجود ندارد و یکی از راه های حل این مشکل ایجاد ساختار RepositoriesTX دیگر و ذخیره pgx.Tx در آن به جای *pgxpool.Pool است، اما انجام این کار ممکن است جدید را به همراه داشته باشد. ممکن است مجبور باشیم روش WithTransaction را به ترتیب برای هر دوی آنها پیاده سازی کنیم.

func (r *Repositories) WithTransaction(ctx context.Context, fn func(qtx *Repositories) (err error)) (err error) {
    var tx pgx.Tx
    if r.tx != nil {
        tx, err = r.tx.Begin(ctx)
    } else {
        tx, err = r.pool.Begin(ctx)
    }
    if err != nil {
        return err
    }
    defer func() {
        if e := tx.Rollback(ctx); e != nil && !errors.Is(e, pgx.ErrTxClosed) {
            err = e
        }
    }()

    if err := fn(NewRepositoriesTx(tx)); err != nil {
        return err
    }

    return tx.Commit(ctx)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تفاوت اصلی این روش با WithTransaction پیاده‌سازی شده در فصل قبل این است که به جای جهانی روی *Repositories پیاده‌سازی می‌شود، بنابراین می‌توانیم تراکنش‌های تودرتو را از طریق pgx.Tx در (r *Repositories) شروع کنیم.

هنگامی که یک تراکنش شروع نشده است، می‌توانیم Repositories.WithTransaction را برای شروع یک تراکنش جدید فراخوانی کنیم.

err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error {

    return nil
})
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تراکنش های چند سطحی نیز مشکلی ندارند و پیاده سازی آن بسیار آسان است.

err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error {
    // 假设此处进行了一些数据操作
    // 然后,开启一个嵌套事务
    return tx.WithTransaction(ctx, func(tx *db.Repositories) error {
        // 这里可以在嵌套事务中进行一些操作
        return nil
    })
})
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این طرح کپسوله‌سازی به طور موثر اتمی بودن عملیات را تضمین می‌کند، حتی اگر هر یک از عملیات‌ها با شکست مواجه شود، کل تراکنش به عقب بازگردانده می‌شود و در نتیجه ثبات داده‌ها را تضمین می‌کند.

نتیجه

این مقاله راه حلی را برای کپسوله کردن تراکنش های پایگاه داده SQLC با استفاده از Go و کتابخانه pgx معرفی می کند.

هسته ساختار Repositories است که رابط پرس و جو SQLC و منطق پردازش تراکنش را در بر می گیرد. با متد WithTransaction، می‌توانیم یک تراکنش فرعی جدید را روی یک تراکنش موجود شروع کنیم یا یک تراکنش جدید را در استخر اتصال شروع کنیم و اطمینان حاصل کنیم که تراکنش پس از بازگشت عملکرد برگشت داده می‌شود.

سازنده NewRepositories و NewRepositoriesTx به ترتیب برای ایجاد نمونه های مخازن معمولی و درون تراکنش استفاده می شود.

این می تواند چندین عملیات پایگاه داده را در یک تراکنش محصور کند، اگر هر عملیاتی با شکست مواجه شود، تراکنش برگردانده می شود و قابلیت نگهداری و خوانایی کد را بهبود می بخشد.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا