Golang dan evolusi interaksi basis data

Pada artikel ini saya ingin meringkas masalah bekerja dengan database menjalankan golang. Saat memecahkan masalah sederhana, masalah ini biasanya tidak terlihat. Ketika proyek tumbuh, begitu pula masalahnya. Yang paling topikal dari mereka:


  • Mengurangi konektivitas aplikasi database
  • Pembuatan log mode debug
  • Bekerja dengan replika

Artikel ini didasarkan pada paket github.com/adverax/echo/database/sql. Semantik menggunakan paket ini sedekat mungkin dengan paket database / sql standar, jadi saya tidak berpikir bahwa ada orang yang akan mengalami masalah dalam menggunakannya.


Lingkup


Sebagai aturan, sistem besar mencoba untuk terhubung secara longgar dengan area tanggung jawab yang jelas dari setiap komponen sistem. Oleh karena itu, pola desain penerbit / pelanggan dipraktikkan secara luas. Pertimbangkan contoh kecil mendaftarkan pengguna baru dalam sistem.


package main import "database/sql" type User struct { Id int64 Name string Language string } type Manager struct { DB *sql.DB OnSignup func(db *sql.DB, user *User) error } func (m *Manager) Signup(user *User) (id int64, err error) { id, err = m.insert(user) if err != nil { return } user.Id = id err = m.OnSignup(m.DB, user) return } func (m *Manager) insert(user *User) (int64, error) { res, err := m.DB.Exec("INSERT ...") if err != nil { return 0, err } id, err := res.LastInsertId() if err != nil { return 0, err } return id, err } func main() { manager := &Manager{ // ... OnSignup: func(db *sql.DB, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } } 

Dalam contoh ini, kami terutama tertarik pada acara OnSignup. Untuk mempermudah, pawang diwakili oleh satu fungsi (dalam kehidupan nyata, semuanya lebih rumit). Dalam tanda tangan acara, kami secara kaku meresepkan jenis parameter pertama, yang biasanya memiliki konsekuensi yang luas.
Misalkan sekarang kita ingin memperluas fungsionalitas aplikasi kita dan seandainya registrasi pengguna berhasil mengirim pesan ke akun pribadinya. Idealnya, pesan harus ditempatkan dalam transaksi yang sama dengan pendaftaran pengguna.


 type Manager struct { DB *sql.DB OnSignup func(tx *sql.Tx, user *User) error } func (m *Manager) Signup(user *User) error { tx, err := m.DB.Begin() if err != nil { return err } defer tx.Rollback() id, err := m.insert(user) if err != nil { return err } user.Id = id err = m.OnSignup(tx, id) if err != nil { return err } return tx.Commit() } func main() { manager := &Manager{ // ... OnSignup: func(db *sql.Tx, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } } 

Seperti yang Anda lihat dari contoh, kami dipaksa untuk mengubah tanda tangan acara. Solusi ini tidak bersih dan menyiratkan bahwa penangan memiliki pengetahuan tentang konteks pelaksanaan query database. Solusi yang jauh lebih bersih adalah dengan menggunakan basis data generik dan antarmuka transaksi - ruang lingkup.


 import "github.com/adverax/echo/database/sql" type Manager struct { DB sql.DB OnSignup func(scope sql.Scope, user *User) error } func (m *Manager) Signup(user *User) error { tx, err := m.DB.Begin() if err != nil { return err } defer tx.Rollback() id, err := m.insert(user) if err != nil { return err } err = m.OnSignup(tx, id) if err != nil { return err } return tx.Commit() } func main() { manager := &Manager{ // ... OnSignup: func(scope sql.Scope, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } } 

Untuk menerapkan pendekatan ini, kita akan memerlukan dukungan untuk transaksi bersarang, karena pawang, pada gilirannya, dapat menggunakan transaksi. Untungnya, ini bukan masalah, karena sebagian besar DBMS mendukung mekanisme SAVEPOINT.


Database dan Konteks


Dalam praktik normal, koneksi ke database tidak diteruskan sebagai parameter, seperti yang ditunjukkan di atas, dan setiap manajer menyimpan tautan ke koneksi ke database. Ini menyederhanakan tanda tangan metode dan meningkatkan keterbacaan kode. Dalam kasus kami, tidak mungkin untuk menghindari ini, karena Anda perlu mentransfer tautan ke transaksi.
Solusi yang agak elegan adalah dengan menempatkan tautan ke transaksi (lingkup) dalam konteks, karena konteks diposisikan sebagai parameter ujung ke ujung. Kemudian kita dapat menyederhanakan kode kita lebih lanjut:


 import ( "context" "github.com/adverax/echo/database/sql" ) type Manager struct { sql.Repository OnSignup func(ctx context.Context, user *User) error } func (m *Manager) Signup(ctx context.Context, user *User) error { return m.Transaction( ctx, func(ctx context.Context, scope sql.Scope) error { id, err := m.insert(user) if err != nil { return err } user.Id = id return m.OnSignup(ctx, user) }, ) } type Messenger struct { sql.Repository } func(messenger *Messenger) onSignupUser(ctx context.Context, user *User) error { _, err := messenger.Scope(ctx).Exec("INSERT ...") return err } func main() { db := ... messenger := &Messenger{ Repository: sql.NewRepository(db), } manager := &Manager{ Repository: sql.NewRepository(db), OnSignup: messenger.onSignup, } err := manager.Signup(&User{...}) if err != nil { panic(err) } } 

Contoh ini menunjukkan bahwa kami telah mempertahankan isolasi lengkap para manajer, meningkatkan keterbacaan kode, dan mencapai kerja bersama mereka dalam satu cakupan.


Dukungan replikasi


Perpustakaan juga mendukung penggunaan replikasi. Semua permintaan tipe Exec dikirim ke Master. Permintaan tipe Slave ditransfer ke Slave yang dipilih secara acak. Untuk mendukung replikasi, cukup tentukan beberapa sumber data:


 func work() { dsc := &sql.DSC{ Driver: "mysql", DSN: []*sql.DSN{ { Host: "127.0.0.1", Database: "echo", Username: "root", Password: "password", }, { Host: "192.168.44.01", Database: "echo", Username: "root", Password: "password", }, }, } db := dsc.Open(nil) defer db.Close() ... } 

Jika Anda menggunakan sumber data tunggal saat membuka database, itu akan dibuka dalam mode normal tanpa overhead tambahan.


Metrik


Seperti yang Anda ketahui, metrik murah, dan log mahal. Karena itu, diputuskan untuk menambahkan dukungan untuk metrik default.


Profil Permintaan dan Pencatatan


Sangat penting untuk mencatat permintaan basis data selama debugging. Namun, saya belum melihat mekanisme penebangan berkualitas tinggi dengan nol biaya produksi. Perpustakaan memungkinkan Anda untuk menyelesaikan masalah ini secara elegan dengan membungkus basis data. Untuk membuat profil basis data, cukup lewati penggerak yang sesuai untuknya:


 func openDatabase(dsc sql.DSC, debug bool) (sql.DB, error){ if debug { return dsc.Open(sql.OpenWithProfiler(nil, "", nil)) } return dsc.Open(nil) } func main() { dsc := ... db, err := openDatabase(dsc, true) if err != nil { panic(err) } defer db.Close() ... } 

Kesimpulan


Paket yang diusulkan memungkinkan Anda untuk memperluas kemungkinan interaksi dengan database, sambil menyembunyikan detail yang tidak perlu. Ini memungkinkan Anda untuk meningkatkan kualitas kode, membiarkannya terkoneksi secara longgar dan transparan, meskipun kompleksitas aplikasi semakin meningkat.

Source: https://habr.com/ru/post/id445908/


All Articles