Сайт на Golang. Часть 4. Подключаемся и взаимодействуем с БД

В предыдущих частях мы обходили стороной вопрос подключения и взаимодействия с базой данных. Однако теперь, когда все основные базовые моменты уже освещены, самое время разобрать и этот пункт. Ведь без БД сегодня сложно представить себе мало-мальски полезный сайт или приложение.

Прежде всего нам необходимо предусмотреть ее наличие. Это может быть как локально запущенный сервер, так и внешний платный или бесплатный сервер с любой из приглянувшихся вам СУБД. Для примера я выбрал MySQL базу на одном из бесплатных хостингов и создал там таблицу «users», используя следующий несложный скрипт:

CREATE TABLE users
(
    id int auto_increment primary key,
    name varchar(32) null,
    surname varchar(32) null
);

И перенес туда список тех самых пользователей, которых мы ранее зашили в виде массива в функции «GetAllUsers()» типа «User».

INSERT INTO `your_db_name`.users (id, name, surname) VALUES (1, 'Джон', 'До');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (2, 'Говард', 'Рорк');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (3, 'Джек', 'Доусон');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (4, 'Лизель', 'Мемингер');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (5, 'Джейн', 'Эйр');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (6, 'Мартин', 'Иден');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (7, 'Джон', 'Голт');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (8, 'Сэмвелл', 'Тарли');
INSERT INTO `your_db_name`.users (id, name, surname) VALUES (9, 'Гермиона', 'Грейнджер');

Подключение к базе данных

Теперь когда у нас есть данные для доступа к базе, а также таблица с которой мы будем работать, мы можем приступать к описанию логики работы с БД.

В папке «app» создадим папку «server» и разместим в ней файл «database.go» со следующим кодом:

package server

import (
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
)

//глобальная переменная с подключением к БД
var Db *sqlx.DB

//функция, инициирующая подключение к БД
func InitDB() (err error) {
	//строка, содержащая данные для подключения к БД в следующем формате:
	//login:password@tcp(host:port)/dbname
	var dataSourceName = "0vfkXXX:6iqvXXX@tcp(remotemysql.com:3306)/0vfkXXX"
	//подключаемся к БД, используя нужный драйвер и данные для подключения
	Db, err = sqlx.Connect("mysql", dataSourceName)
	if err != nil {
		return
	}

	err = Db.Ping()
	return
}

Теперь добавим ее вызов в функции main():

func main() {
	//инициализируем подключение к базе данных
	err := server.InitDB()
	if err != nil {
		log.Fatal(err)
	}

	//создаем и запускаем в работу роутер для обслуживания запросов
	r := httprouter.New()
	routes(r)

	//прикрепляемся хосту и порту для приема и обслуживания входящих запросов
	//вторым параметром передается роутер, который будет работать с запросами
	err = http.ListenAndServe("localhost:4444", r)
	if err != nil {
		log.Fatal(err)
	}
}

Выборка данных

Возвращаемся к файлу «user.go» пакета «model» и переписываем функцию «GetAllUsers()». Стандартный вариант выборки данных выглядит примерно следующим образом:

func GetAllUsers() (users []User, err error) {
	query := `SELECT * FROM users`
	rows, err := server.Db.Queryx(query)
	if err != nil {
		return users, err
	}
	defer rows.Close()

	user := User{}
	for rows.Next() {
		err = rows.StructScan(&user)
		if err != nil {
			return users, err
		}
		users = append(users, user)
	}

	return users, err
}

Но так как мы используем пакет «sqlx«, то нам ничего не мешает выбрать более компактный вариант:

func GetAllUsers() (users []User, err error) {
	query := `SELECT * FROM users`
	err = server.Db.Select(&users, query)
	return
}

Собрав приложение и заглянув на страницу «/users», мы не должны увидеть ничего нового — все тот же список пользователей, только на этот раз источником данных послужила выборка из БД, а не зашитый в коде массив данных.

Добавление записей

В файле «user.go» пакета «model» добавляем функцию-конструктор для нового объекта типа «User» и в добавок функцию для получения объекта из БД по его id (пригодится далее).

func NewUser(name, surname string) *User {
	return &User{Name: name, Surname: surname}
}

func GetUserById(userId string) (u User, err error) {
	query := `SELECT * FROM users WHERE id = ?`
	err = server.Db.Get(&u, query, userId)
	return
}

Затем описываем соответствующий метод, отвечающий за запись нового элемента в нашей таблице :

func (u *User) Add() (err error) {
	query := `INSERT INTO users (name, surname) VALUES (?, ?)`
	_, err = server.Db.Exec(query, u.Name, u.Surname)
	return
}

Теперь в файле «users.go» пакета «controller» описываем логику для хендлера «AddUser()»:

func AddUser(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
	//получаем значение из параметра name, переданного в форме запроса
	name := r.FormValue("name")
	//получаем значение из параметра surname, переданного в форме запроса
	surname := r.FormValue("surname")
	
	//проверяем на пустые значения
	if name == "" || surname == ""  {
		http.Error(rw, "Имя и фамилия не могут быть пустыми", 400)
		return
	}

	//создаем новый объект
	user := model.NewUser(name, surname)
	//записываем нового пользователя в таблицу БД
	err := user.Add()
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}
	
	//возвращаем текстовое подтверждение об успешном выполнении операции
	err = json.NewEncoder(rw).Encode("Пользователь успешно добавлен!")
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}
}

И, наконец, добавляем нужный маршрут (функция «routes()» в файле «main.go»)

r.POST("/user/add", controller.AddUser)

Проверяем через «Insomnia», «Postman» или их аналоги. Радуемся, что все работает.

Далее действуем по аналогичной схеме.

Удаление записей

Добавляем метод:

func (u *User) Delete() (err error) {
	query := `DELETE FROM users WHERE id = ?`
	_, err = server.Db.Exec(query, u.Id)
	return
}

Описываем логику для хендлера:

func DeleteUser(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
	//получаем значение из параметра userId, переданного в строке запроса
	userId := p.ByName("userId")

	//получаем пользователя из БД по его id
	user, err := model.GetUserById(userId)
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}

	//удаляем строку из таблицы
	err = user.Delete()
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}

	//возвращаем текстовое подтверждение об успешном выполнении операции
	err = json.NewEncoder(rw).Encode("Пользователь был успешно удален")
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}
}

Добавляем маршрут:

r.DELETE("/user/delete/:userId", controller.DeleteUser)

Проверяем:

Обновление записей

Добавляем метод:

func (u *User) Update() (err error) {
	query := `UPDATE users SET name = ?, surname = ? WHERE id = ?`
	_, err = server.Db.Exec(query, u.Name, u.Surname, u.Id)
	return
}

Описываем логику для хендлера:

func UpdateUser(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
	//получаем значение из параметра userId, переданного в строке запроса
	userId := p.ByName("userId")
	//получаем значения из параметров name и surname, переданных в форме запроса
	name := r.FormValue("name")
	surname := r.FormValue("surname")
	
	//получаем пользователя из БД по его id
	user, err := model.GetUserById(userId)
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}
	
	//заменяем старые значения на новые
	user.Name = name
	user.Surname = surname
	
	//обновляем данные в таблице
	err = user.Update()
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}
	
	//возвращаем текстовое подтверждение об успешном выполнении операции
	err = json.NewEncoder(rw).Encode("Пользователь был успешно изменен")
	if err != nil {
		http.Error(rw, err.Error(), 400)
		return
	}
}

Добавляем маршрут:

r.POST("/user/update/:userId", controller.UpdateUser)

Проверяем:

Заключение

На этом, кажется, и все. Теперь мы владеем базовой информацией, необходимой для того, чтобы создать свой сайт на Golang (как опубликовать сайт на go читайте здесь). Конечно, остается еще масса интересных тем, таких как авторизация пользователей, отправка почты, взаимодействие с куками, сессией, кэшем и т.д. Возможно, чуть позже я затрону и эти темы. А пока оставляйте комментарии, делитесь своим мнением или информацией о найденных ошибках. Буду благодарен каждому. Адекватная обратная связь всегда приветствуется! =)

P.S. Структура проекта: