Shanraq.org Shanraq.org
Көші-қон: дерекқор тарихы, `create table if not exists` емес
IT

Go тілі: нөлден өз блогыңызға дейін 31-сабақ (барлығы 50)

Көші-қон: дерекқор тарихы, `create table if not exists` емес

Go курсының отыз бірінші сабағы. Схеманы енді бағдарлама қосылғанда жасамайды: нөмірленген көші-қон файлдары, журнал және «бір рет» ережесі пайда болады. Оның үстіне өлшенген тұзақ: default-сыз not null бағанын бос дерекқор қабылдайды, жұмыстағысы қабылдамайды.

Не үшін керек

Өткен сабақтарда схеманы бағдарламаның өзі жасайтын: қосылғанда create table if not exists. Кесте біреу болып, өзгермей тұрғанда бұл жұмыс істейді.

Кестені өзгерту керек болған күні бәрі бұзылады. Айталық, мақалаларға updated_at бағаны керек болды. if not exists бұл жерде көмектеспейді: кесте бар, ал ондағы баған жоқ. Сонда кодқа alter table жазады — бірінші рет өтеді, екінші рет өтпейді:

$ sqlite3 blog.db "alter table articles add column updated_at text not null default '';"
$ sqlite3 blog.db "alter table articles add column updated_at text not null default '';"
Error: in prepare, duplicate column name: updated_at

Әдетте одан кейін жамау жазады: мұндай баған бар ма екенін қарап, жоқ болса ғана қосу. Өзгеріс біреу болғанда жамау жұмыс істейді. Олар онға жетіп, реті мәнге ие бола бастағанда, бір күні жұмыстағы серверде жартысы қолданылғаны, ал қайсысы екенін ешкім білмейтіні белгілі болады.

Көші-қон — схема өзгерістерін кодпен бірдей ұстау тәсілі: бөлек файлдармен, кезекпен, не қолданылғаны жазылып отырады.

Бірден тұтас

Жаңа қалта, go mod init sabaq28, драйвер сол баяғы. main.go қасында — migrations қалтасы, ондағы үш файл:

migrations/0001_articles.sql

create table articles (
    id    integer primary key,
    slug  text not null unique,
    title text not null,
    body  text not null
) strict;

migrations/0002_updated_at.sql

alter table articles add column updated_at text not null default '';

migrations/0003_updated_at_index.sql

create index articles_updated_at on articles (updated_at);

main.go

package main

import (
	"database/sql"
	"embed"
	"fmt"
	"io/fs"
	"log"
	"path"
	"sort"
	"strings"

	_ "modernc.org/sqlite"
)

//go:embed migrations/*.sql
var files embed.FS

func main() {
	db, err := sql.Open("sqlite", "blog.db?_pragma=foreign_keys(1)")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	if err := migrate(db); err != nil {
		log.Fatal("көші-қон: ", err)
	}

	var version string
	err = db.QueryRow(`select coalesce(max(version), 'жоқ') from schema_migrations`).Scan(&version)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("схема нұсқасы:", version)
}

func migrate(db *sql.DB) error {
	_, err := db.Exec(`create table if not exists schema_migrations (
	    version    text primary key,
	    applied_at text not null default (datetime('now'))
	) strict`)
	if err != nil {
		return err
	}

	names, err := fs.Glob(files, "migrations/*.sql")
	if err != nil {
		return err
	}
	sort.Strings(names)

	for _, name := range names {
		version := strings.TrimSuffix(path.Base(name), ".sql")

		var applied int
		err := db.QueryRow(`select count(*) from schema_migrations where version = ?`, version).Scan(&applied)
		if err != nil {
			return err
		}
		if applied == 1 {
			fmt.Println("бұрын қолданылған:", version)
			continue
		}

		body, err := files.ReadFile(name)
		if err != nil {
			return err
		}

		tx, err := db.Begin()
		if err != nil {
			return err
		}
		if _, err := tx.Exec(string(body)); err != nil {
			tx.Rollback()
			return fmt.Errorf("%s: %w", version, err)
		}
		if _, err := tx.Exec(`insert into schema_migrations (version) values (?)`, version); err != nil {
			tx.Rollback()
			return fmt.Errorf("%s: %w", version, err)
		}
		if err := tx.Commit(); err != nil {
			return fmt.Errorf("%s: %w", version, err)
		}
		fmt.Println("қолданылды:", version)
	}
	return nil
}

Бірінші іске қосу:

қолданылды: 0001_articles
қолданылды: 0002_updated_at
қолданылды: 0003_updated_at_index
схема нұсқасы: 0003_updated_at_index

Екінші — сол дерекқорда:

бұрын қолданылған: 0001_articles
бұрын қолданылған: 0002_updated_at
бұрын қолданылған: 0003_updated_at_index
схема нұсқасы: 0003_updated_at_index

Талдау

Файл атындағы нөмір — сол нұсқа

0001, 0002, 0003. Файлдарды fs.Glob оқиды, ретін sort.Strings белгілейді — жолдардың кәдімгі сұрыпталуы. Сондықтан нөмірді бас жағындағы нөлдермен жазады: онсыз 9-дан кейін 10 келер еді, ал жолдар таңбалап салыстырылады, сол себепті 10 9-дың алдына тұрып қалады.

Нөмірден кейінгі ат ешнәрсені шешпейді, ол адамдар үшін. Жарты жылдан кейін 0002_updated_at сізге 0002-ден көбірек айтады.

Басты ереже біреу: қолданылған файлды түзетпейді. Бағанның атын өзгерткіңіз келді ме — бұл жаңа көші-қон, ескісіне түзету емес. Ескісі бөтен машиналарда да, жұмыстағы серверде де қолданылып қойған, оны ол жерде ешкім қайта қолданбайды.

Журнал: қолданылғанды есте сақтайтын кесте

schema_migrations — сол дерекқордағы кәдімгі кесте. Қолданылған әр файлға бір жол: нұсқа мен уақыт.

Ол қасындағы файлда емес, дерекқордың өзінде, әрі бұл маңызды. Дерекқордың көшірмесін басқа машинаға журналымен бірге алып барады, сондықтан онда дәл жетіспейтіні қолданылады.

Елестетіп көріңіз. Егу журналы. Онда егулердің өзі жоқ — тек жасалғандардың тізімі бар, әрі дәл сол екінші рет сондай егу салдырмайды.

embed: көші-қондар бағдарламаның ішінде

//go:embed migrations/*.sql файлдарды жиналған бағдарламаның ішіне салады — CSS пен сурет туралы сабақтағы статикамен бірдей тәсіл.

Онсыз серверге migrations қалтасын бинарлықтың қасында алып барып, оның нұсқасы бағдарламаның нұсқасымен сәйкес келуін қадағалауға тура келер еді. embed-пен олар ажырай алмайды: бұл — бір файл.

Транзакция: сынған көші-қон із қалдырмайды

migrations ішіне бірнеше пәрмені бар файл салуға болады. Сонда қауіп пайда болады: бірінші пәрмен өтті, екіншісі құлады — схема ортасында қалды.

Мұны тексерейік. 0005_broken.sql файлы, ондағы екінші жол біріншісін қайталайды:

alter table comments add column author text not null default '';
alter table comments add column author text not null default '';

Іске қосу:

бұрын қолданылған: 0004_comments
2026/09/06 13:23:38 көші-қон: 0005_broken: SQL logic error: duplicate column name: author (1)
exit status 1

Ал енді ең бастысы — дерекқорда не қалды:

$ sqlite3 blog.db "select version from schema_migrations order by version;"
0001_articles
0002_updated_at
0003_updated_at_index
0004_comments

$ sqlite3 blog.db "select name from pragma_table_info('comments');"
id
article_id
body

Файлдың бірінші пәрмені қосқанымен, author бағаны мүлде жоқ. Транзакция оны да, журналдағы жазбаны да артқа қайтарды: tx.Begin, ал қате болғанда tx.Rollback — дерекқор не жерден бастаса, соған оралды. Файлды түзетіп, қайта іске қосыңыз — көші-қон таза жерден қолданылады.

Бұлай бәрінде бола бермейді. SQLite мен Postgres-те create table мен alter table артқа қайтарылады, ал MySQL-де қайтарылмайды: онда әрбір осындай пәрмен басталған транзакцияны байқаусыз аяқтайды, сондықтан сынған көші-қон үлгергенінің дәл жартысын қалдырады. MySQL 8-де бөлек пәрмен бөлінбейтін болды, бірақ көші-қон түгелдей емес.

Begin, Commit және Rollback-ті келесі сабақта егжей-тегжейлі қараймыз; мұнда олардың неге тұрғанын түсінсеңіз жеткілікті.

SQLite-тағы alter table не істей алады, не істей алмайды

Көп нәрсе істей алмайды, әрі мұны алдын ала білген жөн:

alter table articles rename column title to heading;   -- істейді
alter table articles drop column updated_at;           -- істейді
alter table articles alter column heading type blob;   -- мұндай пәрмен жоқ

Соңғы жол — дерекқордың қатесі емес, жоқ мүмкіндік:

Error: in prepare, near "alter": syntax error

SQLite-та бағанның типін өзгерту немесе not null-ды алып тастау бір пәрменмен мүмкін емес. Ресми жол — он екі қадамнан тұратын рәсім: қасында керекті түрдегі жаңа кесте жасау, деректі insert ... select арқылы құю, ескісін өшіру және жаңасының атын өзгерту. Мұның бәрі — бір көші-қонның ішінде, әрі бір нәрсе дұрыс болмаса, дерекқор мұның бәрін артқа қайтара алады.

Postgres-те мұндай пәрмен бар, әрі бұл — SQLite-тан көшу өмірді байқатып жеңілдететін санаулы жерлердің бірі.

not null: бос дерекқор кешіреді, жұмыстағысы кешірмейді

Дәл жұмыстағы серверде іске қосылатын тұзақ.

Бос кестеде not null бағаны әдепкі мәнсіз үнсіз қосыла береді:

alter table t add column b text not null;

Ең болмаса бір жол пайда болса, сол пәрмен былай жауап береді:

Error: stepping, Cannot add a NOT NULL column with default value NULL

Логикасы қарапайым: бұрыннан бар жолдарда жаңа баған жоқ, дерекқор оны бір нәрсемен толтыруға міндетті, ал толтыратын ештеңе жоқ. Сіздің машинаңызда дерекқор бос, көші-қон өтеді, сіз оны жібересіз — ол дерек бар жерде құлайды.

Осыдан ереже шығады: not null — әрқашан default-пен. Бос жол, нөл, datetime('now') — мағыналы кез келген нәрсе.

Артқа қайтармаймыз

Дайын құралдар «көші-қонды артқа қайтара» алады: up файлының қасына оны болдырмайтын down файлын қояды. Қағаз жүзінде әдемі.

Іс жүзінде артқа қайтару болғанды қалпына келтірмейді. Бағанды өшірген көші-қон деректі де алып кетті, ал down оны тірілтпейді. Сондықтан жұмыстағы жобаларда көбіне тек алға жүреді: қателестіңіз бе — түзейтін келесі көші-қонды жазасыз.

Курста down жазбаймыз. Ондай нәрсе бар екенін және оған неге сенбеу керегін білсеңіз жеткілікті.

Тағы бір нәрсе: көші-қонды орындайтын кодты құрылысын түсіну үшін өзіміз жаздық — мұнда бүкіл тетік бір функцияға сыйып тұр. Нағыз жобада дайынын алады, көбінесе golang-migrate немесе goose; олар осының бәрін істейді, оның үстіне қатар іске қосылған екі сервер бір көші-қонды екеулеп қолдана бастамауы үшін құлыптау қосады.

Сабақ картасы

Сабақ картасы: кезекпен тұрған файлдар, журнал және бір транзакция

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.

  1. Көші-қон нөмірін неге 1 емес, 0001 деп жазады?
  2. Екі пәрменнен тұратын көші-қон екіншісінде құласа, дерекқорда не қалады?
  3. default-сыз not null неге сізде өтеді де, серверде құлайды?

Тапсырма

Міндетті. Блогты көші-қонға көшіріңіз. Схеманы енді Open жасамайды; оның орнына migrations қалтасы пайда болады, бірінші файл қазіргі articles кестесін қайталайды, ал екіншісі updated_at бағанын қосады, оны Update datetime('now') арқылы толтырады. Мақалалары бар дерекқорда тексеріңіз: ескі жолдар аман қалуы тиіс.

Көші-қонды орындайтын код step-16 ішінде жазылып қойған — өзіңіз жасап болғаннан кейін онымен салыстырыңыз.

Қалауыңызша.

  • migrate-тен транзакцияны алып тастап, сынған файлмен тәжірибені қайталаңыз. Енді кестеде не қалатынын қараңыз.
  • schema_migrations-қа қолдану ұзақтығын жазатын баған қосып, оны қосылғанда шығарыңыз.
  • body бағанының атын body_md-ге өзгертетін көші-қон жазып, Store-ды жаңартыңыз. Ескі мақалалардың орнында тұрғанына көз жеткізіңіз.

Блогта бұл қайда тұрады

Open схеманы білуін қояды: оның ісі — дерекқорды ашу, ал кестелердің пішінін migrations ішіндегі файлдар белгілейді. Енді блогты жазылғанды жоғалтпай өзгертуге болады.

Қарыздар. Сұраныстар әлі де жолдардан құралады, әрі келесі сабақта олай істеуге болмайтыны белгілі болады. Update барлық өрісті бірден жазады. Ал көші-қон кезінде құлыптауымыз жоқ — бір сервер үшін бұл қорқынышты емес, екеуі үшін қорқынышты.

Жауаптар

Жауаптарды көрсету
  1. Себебі файлдар жолдар ретінде, таңбалап сұрыпталады. Бас жағындағы нөлдерсіз 10 9-дың алдына тұрады да, көші-қондар жазылған ретімен емес, басқаша қолданылады.
  2. Ештеңе қалмайды: транзакция бірінші пәрменнің үлгергенін де, журналдағы жазбаны да артқа қайтарады. Көші-қон түгелдей қолданылмаған күйде қалады, ал файл түзетілгеннен кейін қайтадан қолданылады.
  3. Себебі сізде кесте бос, ал серверде онда жолдар бар. Бұрыннан бар жолдарға дерекқордың жаңа бағанды толтыратын ештеңесі жоқ, сондықтан ол бас тартады: Cannot add a NOT NULL column with default value NULL.

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.