Описание типа Conn database/sql

Conn представляет собой одно соединение с базой данных, а не пул соединений с базой данных.
type Conn struct {
// содержит отфильтрованные или неэкспонированные поля
}

Conn представляет собой одно соединение с базой данных, а не пул соединений с базой данных. Предпочтительнее запускать запросы из БД, если нет особой необходимости в постоянном соединении с одной базой данных.

Conn должен вызвать Conn.Close, чтобы вернуть соединение в пул баз данных, и может делать это одновременно с выполняющимся запросом.

После вызова Conn.Close все операции над соединением завершаются с ошибкой ErrConnDone.

func (*Conn) BeginTx

func (c *Conn) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)

BeginTx начинает транзакцию.

Предоставленный контекст используется до тех пор, пока транзакция не будет зафиксирована или откачена. Если контекст будет отменен, пакет sql откатит транзакцию. Tx.Commit вернет ошибку, если контекст, предоставленный BeginTx, будет отменен.

Предоставленный параметр TxOptions является необязательным и может быть равен nil, если следует использовать значения по умолчанию. Если используется уровень изоляции не по умолчанию, который драйвер не поддерживает, будет возвращена ошибка.

func (*Conn) Close

func (c *Conn) Close() error

Close возвращает соединение в пул соединений. Все операции после Close возвращаются с ошибкой ErrConnDone. Close безопасно вызывать одновременно с другими операциями, и он будет блокироваться до тех пор, пока все остальные операции не завершатся. Может оказаться полезным сначала отменить любой используемый контекст, а затем вызвать Close непосредственно после него.

func (*Conn) ExecContext

func (c *Conn) ExecContext(ctx context.Context, query string, args ...any) (Result, error)

ExecContext выполняет запрос, не возвращая никаких строк. В качестве args используются любые параметры-заполнители в запросе.

Пример
package main

import (
	"context"
	"database/sql"
	"log"
	"os"

	_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)

func main() {
	// 1. Инициализация подключения к БД
	db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
	if err != nil {
		log.Fatal("Failed to connect to database:", err)
	}
	defer db.Close()

	// 2. Проверка соединения
	ctx := context.Background()
	if err := db.PingContext(ctx); err != nil {
		log.Fatal("Failed to ping database:", err)
	}

	// 3. Получение выделенного соединения
	conn, err := db.Conn(ctx)
	if err != nil {
		log.Fatal("Failed to get connection:", err)
	}
	defer func() {
		if err := conn.Close(); err != nil {
			log.Printf("Warning: error closing connection: %v", err)
		}
	}()

	// 4. Выполнение транзакции
	id := 41
	tx, err := conn.BeginTx(ctx, nil)
	if err != nil {
		log.Fatal("Failed to begin transaction:", err)
	}

	// 5. Выполнение UPDATE в транзакции
	result, err := tx.ExecContext(ctx,
		`UPDATE balances SET balance = balance + 10 WHERE user_id = ?`,
		id,
	)
	if err != nil {
		tx.Rollback()
		log.Fatal("Failed to update balance:", err)
	}

	// 6. Проверка количества измененных строк
	rows, err := result.RowsAffected()
	if err != nil {
		tx.Rollback()
		log.Fatal("Failed to get rows affected:", err)
	}

	if rows != 1 {
		tx.Rollback()
		log.Printf("Expected single row affected, got %d rows affected", rows)
		os.Exit(1) // Более мягкое завершение чем log.Fatal
	}

	// 7. Фиксация транзакции
	if err := tx.Commit(); err != nil {
		log.Fatal("Failed to commit transaction:", err)
	}

	log.Println("Balance updated successfully")
}

func (*Conn) PingContext

func (c *Conn) PingContext(ctx context.Context) error

PingContext проверяет, что соединение с базой данных все еще живо.

func (*Conn) PrepareContext

func (c *Conn) PrepareContext(ctx context.Context, query string) (*Stmt, error)

PrepareContext создает подготовленный запрос для последующих запросов или выполнений. Несколько запросов или выполнений могут быть запущены одновременно из полученного утверждения. Вызывающая сторона должна вызвать метод *Stmt.Close оператора, когда он больше не нужен.

Предоставленный контекст используется для подготовки утверждения, а не для его выполнения.

func (*Conn) QueryContext

func (c *Conn) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)

QueryContext выполняет запрос, возвращающий строки, обычно SELECT. В args указываются любые параметры-заполнители запроса.

func (*Conn) QueryRowContext

func (c *Conn) QueryRowContext(ctx context.Context, query string, args ...any) *Row

QueryRowContext выполняет запрос, который, как ожидается, вернет не более одной строки. QueryRowContext всегда возвращает значение non-nil. Ошибки откладываются до вызова метода *Row.Scan. Если запрос не выбрал ни одной строки, то *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.

func (*Conn) Raw

func (c *Conn) Raw(f func(driverConn any) error) (err error)

Raw выполняет f, раскрывая базовое соединение драйвера на время выполнения f. DriverConn не должен использоваться вне f.

После того как f вернется и err не будет driver.ErrBadConn, Conn будет продолжать использоваться до вызова Conn.Close.

Разбираем метод Raw() для работы с низкоуровневыми соединениями

Метод Raw() в database/sql позволяет получить прямое доступ к драйвер-специфичному соединению, минуя абстракции пакета database/sql. Это нужно для использования специфичных функций драйвера, которые не доступны через стандартный API.

Что такое f в этом контексте?

f - это ваша функция-колбэк, которая получает доступ к нативному соединению драйвера. Она должна иметь сигнатуру:

func(driverConn any) error

Зачем это нужно?

  1. Использование специфичных функций драйвера (например, PostgreSQL-специфичные команды)
  2. Оптимизация производительности для критичных участков кода
  3. Работа с нестандартными возможностями БД

Простой пример (PostgreSQL)

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	_ "github.com/lib/pq"
)

func main() {
	db, err := sql.Open("postgres", "user=postgres dbname=test sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	conn, err := db.Conn(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// Используем Raw для доступа к нативному соединению PostgreSQL
	err = conn.Raw(func(driverConn interface{}) error {
		// Приводим к типу, который ожидаем (для PostgreSQL)
		pgConn, ok := driverConn.(driver.Conn) // Здесь нужен ваш тип соединения
		if !ok {
			return fmt.Errorf("unexpected driver connection type")
		}

		// Пример: выполняем LISTEN для PostgreSQL-нотификаций
		// Это специфичная функция PostgreSQL, недоступная через стандартный API
		_, err := pgConn.(*pq.Conn).Exec("LISTEN channel_name")
		return err
	})

	if err != nil {
		log.Fatal(err)
	}
}

Более практичный пример (MySQL)

package main

import (
	"context"
	"database/sql"
	"log"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, err := sql.Open("mysql", "user:password@/dbname")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	conn, err := db.Conn(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// Получаем низкоуровневое соединение MySQL
	err = conn.Raw(func(driverConn interface{}) error {
		// Приводим к ожидаемому типу соединения MySQL
		mysqlConn, ok := driverConn.(*mysql.Conn) // Требуется import драйвера
		if !ok {
			return fmt.Errorf("expected mysql.Conn, got %T", driverConn)
		}

		// Используем специфичные методы MySQL
		fmt.Println("Server version:", mysqlConn.GetServerVersion())
		fmt.Println("Connection ID:", mysqlConn.GetConnectionID())
		
		// Можно выполнить специфичные команды
		return mysqlConn.Ping()
	})

	if err != nil {
		log.Fatal(err)
	}
}

Важные предупреждения:

  1. Тип соединения зависит от драйвера:

    • PostgreSQL: *pq.Conn
    • MySQL: *mysql.Conn
    • SQLite: зависит от используемого драйвера
  2. Безопасность:

    • Не сохраняйте соединение вне функции f
    • Все ошибки должны быть обработаны
  3. Совместимость:

    • Код становится зависимым от конкретного драйвера
    • Может сломаться при смене драйвера

Когда стоит использовать Raw()?

  1. Когда вам нужен доступ к специфичным функциям СУБД
  2. Для оптимизации критических участков
  3. При работе с нестандартными возможностями (нотификации, специфичные команды)

В большинстве случаев стандартного API database/sql достаточно, и Raw() не понадобится.