Go: SQL

Теория: Подготовленные выражения

Когда приложение много раз выполняет один и тот же SQL-запрос, меняя только значения параметров, ему не нужно каждый раз проходить полный цикл: отправить текст SQL, дождаться анализа, построения структуры запроса и только потом передать параметры. Вместо этого оно вызывает PrepareContext() — метод, который создаёт объект Stmt.Этот объект представляет подготовленное выражение и позволяет многократно выполнять один и тот же запрос с разными параметрами

В случае использования db.PrepareContext() выражение не закрепляется жёстко за одним соединением: при выполнении драйвер может использовать разные соединения из пула и при необходимости подготавливать выражение на каждом из них отдельно. После подготовки приложение выполняет только подстановку параметров и запуск выражения, как правило, без повторной передачи полного текста SQL при каждом выполнении (конкретное поведение зависит от драйвера). Такой подход особенно удобен в циклах, синхронизациях и любых операциях, где запрос повторяется десятки или сотни раз.

ctx := context.Background()

// Драйвер подготавливает выражение (часто с передачей текста SQL на сервер)
stmt, err := db.PrepareContext(ctx, `
	UPDATE products
	   SET price = $1
	 WHERE id = $2
`)
if err != nil {
	log.Fatal(err)
}
defer stmt.Close() // освобождает ресурсы подготовленного выражения на стороне драйвера/БД

// Данные, которые нужно обработать тем же SQL.
updates := []struct {
	ID    int
	Price int
}{
	{1, 95000},
	{2, 87000},
	{3, 125000},
}

// Повторное выполнение одного и того же выражения
// с разными значениями параметров.
for _, u := range updates {
	_, err := stmt.ExecContext(ctx, u.Price, u.ID)
	if err != nil {
		log.Fatal(err)
	}
}

Подготовленное выражение для запросов, которые возвращают строки

Подготовленное выражение можно использовать не только для изменений, но и для запросов, которые возвращают строки. Программа создаёт Stmt один раз, а затем вызывает QueryContext() на том же объекте столько раз, сколько нужно. Каждый вызов открывает новый поток результатов, и приложение считывает строки через Next() и Scan(). Объект Rows удерживает соединение занятым до тех пор, пока не будет вызван rows.Close() или пока не будут прочитаны все строки. Поэтому поток результатов нужно закрывать после каждой выборки, иначе соединения из пула могут временно исчерпаться.

Если закрытие перенести в defer внутри цикла, незакрытые Rows будут накапливаться и удерживать соединения из пула до выхода из функции, а не освобождаться после каждой итерации. Поэтому поток закрывают вручную, сразу после использования, а затем переходят к следующему набору параметров.

ctx := context.Background()

// Подготавливаем выражение, которое будет выполнять выборку.
stmt, err := db.PrepareContext(ctx, `
	SELECT id, name, price
	  FROM products
	 WHERE price >= $1
`)
if err != nil {
	log.Fatal(err)
}
defer stmt.Close()

// Несколько значений, для каждого из которых нужна своя выборка.
limits := []int{50000, 100000, 150000}

for _, limit := range limits {
	// Каждый вызов QueryContext создаёт собственный поток строк.
	rows, err := stmt.QueryContext(ctx, limit)
	if err != nil {
		log.Fatal(err)
	}

	for rows.Next() {
		var (
			id    int
			name  string
			price int
		)

		// Считываем очередную строку результата.
		if err := rows.Scan(&id, &name, &price); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("%d: %s — %d₽\n", id, name, price)
	}

	// Поток нужно закрыть сразу, а не откладывать в defer.
	rows.Close()

	// После закрытия можно проверить rows.Err().
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}
}

Как Stmt взаимодействует с соединениями

Подготовленное выражение, созданное через db.PrepareContext(), является абстракцией на уровне database/sql. Оно может выполняться через разные соединения из пула, и соединение выделяется только на время конкретного вызова ExecContext() или QueryContext(). После завершения операции и закрытия Rows соединение возвращается в пул.

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

ctx := context.Background()

// Создаём подготовленное выражение через DB.
// Оно сможет выполняться через разные соединения из пула.
stmt, err := db.PrepareContext(ctx, `
	INSERT INTO logs(message, created_at)
	VALUES($1, NOW())
`)
if err != nil {
	log.Fatal(err)
}
defer stmt.Close()

messages := []string{"one", "two", "three"}

for _, m := range messages {
	_, err := stmt.ExecContext(ctx, m)
	if err != nil {
		log.Fatal(err)
	}
}

Подготовленное выражение внутри транзакции

Когда программа открывает транзакцию, все операции выполняются через одно соединение, поэтому подготовленное выражение оказывается привязанным к тому же подключению. Такой подход удобен, когда нужно выполнить серию связанных действий: транзакция обеспечивает целостность, а Stmt уменьшает накладные расходы на повторяющийся SQL. Последовательность всегда одна и та же: BeginTx()PrepareContext() на txExecContext()/QueryContext()Commit().

Подготовленные выражения, созданные через tx.PrepareContext(), действительны только в рамках данной транзакции и используют то же соединение. После Commit() или Rollback() такое выражение больше нельзя использовать, и его следует закрыть для освобождения связанных ресурсов, потому что соединение транзакции возвращается в пул и её контекст завершён.

ctx := context.Background()

tx, err := db.BeginTx(ctx, nil)
if err != nil {
	log.Fatal(err)
}
defer tx.Rollback()

stmt, err := tx.PrepareContext(ctx, `
	UPDATE orders
	   SET status = $1
	 WHERE id = $2
`)
if err != nil {
	log.Fatal(err)
}
defer stmt.Close()

updates := []struct {
	ID     int
	Status string
}{
	{10, "processing"},
	{11, "processing"},
	{12, "processing"},
}

for _, u := range updates {
	_, err := stmt.ExecContext(ctx, u.Status, u.ID)
	if err != nil {
		log.Fatal(err)
	}
}

if err := tx.Commit(); err != nil {
	log.Fatal(err)
}

Время жизни подготовленного выражения и закрытие ресурсов

Время жизни подготовленного выражения определяется логикой приложения: его используют, пока запрос остаётся востребованным, и закрывают, когда он больше не нужен. Программа открывает Stmt в одном участке кода, использует его для серии операций и затем закрывает через Close(), чтобы освободить ресурсы подготовленного выражения на стороне драйвера и базы данных. Открытый Stmt, созданный через DB, сам по себе не удерживает соединение постоянно. Соединения удерживаются активными объектами Rows во время чтения результатов, а также на время выполнения запросов.

Объект *sql.Stmt безопасен для конкурентного использования несколькими горутинами. Часто используемые подготовленные выражения допускается кэшировать и переиспользовать в разных частях приложения при условии корректного управления их жизненным циклом.

func SaveEvents(ctx context.Context, db *sql.DB, events []string) error {
	stmt, err := db.PrepareContext(ctx, `
		INSERT INTO events(message, created_at)
		VALUES($1, NOW())
	`)
	if err != nil {
		return err
	}
	defer stmt.Close() // выражение живёт только внутри этой функции

	for _, msg := range events {
		if _, err := stmt.ExecContext(ctx, msg); err != nil {
			return err
		}
	}

	return nil
}

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845