Сайт на 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. Структура проекта:

16 Comments

  1. Здравствуйте! А будет ли продолжение уроков на го?

    1. Привет, Артем! Цикл задумывался с целью рассказать о самом минимуме, который нужен для создания сайта на golang. Писать о чем-то большем было в планах, но времени не хватило. А о чем тебе интересно было бы еще почитать?

      1. Здравствуйте, спасибо, что ответили! Было бы очень интересно про авторизацию и регистрацию пользователя почитать

        1. Интересная тема! Не обещаю, но попробую выделить на это время в ближайшие 1-2 месяца 😉

  2. Подскажите, пожалуйста, откуда взялись файлы jquery и popper? Они видны в структуре проекта, но в Bootstrap этих файлов нет.

  3. Здравствуйте, не работает ссылка (чтобы создать свой сайт на Golang (как опубликовать сайт на go читайте здесь). Есть возможность у вас исправить ?

    1. Исправил! Ссылка вела на старый домен, надеюсь, ты догадался поискать статью про публикацию на текущем домене сайта )

  4. Как начинающему backend Go мне очень пригодились Ваши статьи! Пожалуйста, не забрасывайте этот проект, и огромное спасибо Вам за труды!

    1. Владислав, спасибо за обратную связь! Постараюсь ) О чем еще было бы интересно почитать?

      1. Я не Владислав, но мне интересно про куки и сессии

  5. Как лучше всего переделать логику InitDB() для работы с PostgreSQL? Возможно ли использовать пакет sqlx?

    Я смотрел как работает sqlx.Connect(), первым параметром функция требует driverName, я нашел список этих драйверов на https://github.com/golang/go/wiki/SQLDrivers и там другие пакеты, но как их вписать в текущую логику непонятно.

    1. Пакет sqlx использовать возможно. Попробуй вот этот драйвер “github.com/jackc/pgx/v4/stdlib” или любой другой подходящий. Это все гуглится.
      Как лучше переделать – вопрос слишком обширный. Я бы на твоем месте сначала просто переделал так, чтобы просто заработало, а потом вносил улучшения.
      Сильно переделывать логику InitDB() не потребуется, но тебе придется внести изменения в команды создания таблиц, записи, чтения и удаления данных (например, вместо “?” там следует использовать “$”).

      В своей статье я привел простой пример, все остальное только в твоих руках 🙂

Leave a Comment

Your email address will not be published. Required fields are marked *