Рубріки: Back-end

Как избежать применения ORM для Go, используя чистый SQL

Павло Бєлавін

Если вы — инженер-программист, который опробовал множество различных языков и фреймворков, то, скорее всего, вы сталкивались с мучительной необходимостью изучать новый синтаксис ORM для каждого отдельного языка. Это большая помеха, которая либо замедлит скорость вашей работы, либо вообще лишит желания продолжать ее.

Software Engineer Ухио Гарсиа Андраде в своем блоге пишет, что пора уже перестать изучать отдельный синтаксис ORM для каждого языка. Если же вы уже знаете SQL, то следуя руководствам вроде этого, сможете использовать свои знания применительно ко множеству разных языков.

Прежде всего необходимо настроить среду разработки. Например, установить Postgres или — в качестве альтернативы — Docker. Ниже приведен файл docker-compose.yaml, содержащий необходимые команды для настройки и запуска:

version: '3'
services:
  api:
    restart: on-failure
    build:
        dockerfile: Dockerfile.dev
        context: './'
    environment:
      - USERNAME=postgres
      - PASSWORD=password
      - HOST=db
      - SCHEMA=todos_db
    ports:
      - '8080:8080'
    expose:
      - '8080'
    depends_on:
        - db
    volumes:
      - ./:/app
  db:
    image: postgres:latest
    restart: always
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DATABASE=todos_db
    ports:
      - '5432:5432'
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    expose:
      - '5432'

Если этот файл разместить в корневой папке проекта, то для вступления в силу всех настроек потребуется запустить следующую команду:

docker-compose up

Как видно, в службе db определен том с SQL-сценарием, который будет выполняться при запуске docker-compose. Обратите внимание, что этот сценарий нужно сопоставить с каталогом docker-entrypoint-initdb.d, чтобы контейнер Postgres выполнял его при запуске. Файл Init.sql содержит определение базы данных и таблицы, которые будут использованы в этом обзоре. Вот один из примеров того, как может выглядеть база данных todos:

CREATE DATABASE todos_db;

\connect todos_db;

CREATE TABLE todos(
    id SERIAL NOT NULL PRIMARY KEY,
    description VARCHAR(100),
    priority INT,
    status VARCHAR(100)
);

После запуска контейнера db выполняется приведенный выше сценарий, в ходе которого создаются база данных и таблица todos (если они еще не были созданы).

Подготовив среду, можно переходить к этапу написания кода.

Во-первых, нужно определить конфигурацию доступа приложения Go к базе данных. Единственный пакет, который необходимо для этого импортировать, — это драйвер postgres.

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

Хотя это и не принципиально, но во избежание жестко запрограммированных строк в некоторых частях кода определим ряд констант. Обратите внимание, что в реальных сценариях имена переменных среды имеют свойство быть более предметными.

const (
   dbUsername = "USERNAME"
   dbPassword = "PASSWORD"
   dbHost = "HOST"
   dbSchema = "SCHEMA"
)

Затем эти константы связываются с переменными среды:

var (
Client *sql.DB
username = os.Getenv(dbUsername)
password = os.Getenv(dbPassword)
host = os.Getenv(dbHost)
schema = os.Getenv(dbSchema)
)

Затем остальную часть кода добавим в функцию init(). В среде Go эта функция вызывается только при первом использовании пакета, даже если впоследствии он импортируется другим пакетом. В этой функции мы только установим соединение с базой данных, а затем с помощью метода Ping() проверим правильность ее работы.

package todos_db

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

    _ "github.com/jackc/pgx/stdlib"
)

const (
    dbUsername = "USERNAME"
    dbPassword = "PASSWORD"
    dbHost     = "HOST"
    dbSchema   = "SCHEMA"
)

var (
    Client   *sql.DB
    username = os.Getenv(dbUsername)
    password = os.Getenv(dbPassword)
    host     = os.Getenv(dbHost)
    schema   = os.Getenv(dbSchema)
)

func init() {
    connInfo := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable",
        host, username, password, schema)
    var err error
    Client, err = sql.Open("pgx", connInfo)
    if err != nil {
        panic(err)
    }
    if err = Client.Ping(); err != nil {
        panic(err)
    }
    log.Println("Database ready to accept connections")
}

Теперь перейдем к определению модели, для чего воспользуемся шаблонами DAO и DTO.

Сначала в каталоге model/todos создадим файл todos_dto.go. Шаблон DTO используется для передачи данных между различными модулями приложения.

По сути, это абстрактный интерфейс, посредством которого происходит обмен информацией между DAO и бизнес-сервисами. В среде Go его можно реализовать, создав структуру и определив, каким образом будет закодировано каждое поле в формате JSON. Обратите внимание, что этот файл не должен содержать никаких функциональных алгоритмов, предусматриваемых приложением.

package todos

type Todo struct {
    ID          int64  `json:"id"`
    Description string `json:"description"`
    Priority    int    `json:"priority"`
    Status      string `json:"status"`
}

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

package todos

import (
    "fmt"
    "log"

    "github.com/uxioandrade/go-sql-tutorial/datasources/postgres/todos_db"
)

func (todo *Todo) Get() error {
    stmt, err := todos_db.Client.Prepare("SELECT id, description, priority, status FROM todos WHERE id=$1;")
    if err != nil {
        log.Println(fmt.Sprintf("Error when trying to prepare statement %s", err.Error()))
        log.Println(err)
        return err
    }
    defer stmt.Close()

    result := stmt.QueryRow(todo.ID)

    if err := result.Scan(&todo.ID, &todo.Description, &todo.Priority, &todo.Status); err != nil {
        log.Println("Error when trying to get Todo by ID")
        return err
    }
    return nil
}

func (todo *Todo) Save() error {
    stmt, err := todos_db.Client.Prepare("INSERT INTO todos(description, priority, status) VALUES($1, $2, $3) RETURNING id;")
    if err != nil {
        log.Println("Error when trying to prepare statement")
        log.Println(err)
        return err
    }
    defer stmt.Close()
    var lastInsertID int64
    insertErr := stmt.QueryRow(todo.Description, todo.Priority, todo.Status).Scan(&lastInsertID)
    if insertErr != nil {
        log.Println("Error when trying to save todo")
        return err
    }
    todo.ID = lastInsertID
    log.Println(fmt.Sprintf("Successfully inserted new todo with id %d", todo.ID))
    return nil
}

Вместо прямого вызова методов Exec() или Query(), в нашем случае будут использованы предварительно подготовленные операторы. Хотя оба подхода обладают определенными преимуществами, ряд критериев указывает на то, что в среде Go подготовленные операторы более производительны.

После вызова метода Prepare() с нужным запросом осуществляется проверка на ошибку, а затем откладывается закрытие подготовленного оператора. Если забывать сделать это, то он будет навсегда привязан к данному сеансу подключения. По этой причине после подготовки оператора важно всегда откладывать вызов метода stmt.Close().

Затем в методе Save() выполняется метод QueryRow и возвращается идентификатор метода Scan(). Поскольку идентификатор генерируется базой данных автоматически, то в выполняемом запросе INSERT он будет отсутствовать. В некоторых базах данных, таких как MySQL, предусмотрена возможность получить его с помощью вызова метода LastInsertId() после выполнения запроса. Однако, поскольку мы имеем дело с Postgres, то можем явно запросить возврат сгенерированного идентификатора:

INSERT INTO todos(description, priority, status) VALUES($1, $2, $3) RETURNING id;

Убедившись в отсутствии ошибок, присваиваем полученный идентификатор структуре Todo и завершаем вставку.

Благодаря способу получения идентификатора todo.ID в запросе на вставку, процедура метода Get() будет аналогичной и даже более простой.

Следующий файл понадобится, чтобы проверить, что все работает как надо:

package main

import (
    "log"

    "github.com/uxioandrade/go-sql-tutorial/model/todos"
)

func main() {
    firstTodo := todos.Todo{
        Description: "First todo",
        Priority:    1,
        Status:      "In Progress",
    }
    secondTodo := todos.Todo{
        Description: "Second todo",
        Priority:    3,
        Status:      "Done",
    }
    oldTodo := todos.Todo{
        ID: 1,
    }
    firstTodo.Save()
    secondTodo.Save()
    oldTodo.Get()
    log.Println(oldTodo)
}

Это простая функция, в которой вставляются два экземпляра структуры todos, а затем извлекается тот, который имеет идентификатор 1. С учетом добавленной в шаблон DAO регистрации операций результат работы функции main() может выглядеть следующим образом (обратите внимание, что полученные идентификаторы имеют номера 5 и 6, потому что к этому моменту функция уже несколько раз выполнялась):

Результат работы main.go. Источник: betterprogramming.pub

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

Оригинальный текст перевел Владимир Черный

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023