Если вы — инженер-программист, который опробовал множество различных языков и фреймворков, то, скорее всего, вы сталкивались с мучительной необходимостью изучать новый синтаксис 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.
Оригинальный текст перевел Владимир Черный
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…