Khi bạn nghĩ về Internet trong tưởng tượng tất cả đều là các trang web nhìn thật đẹp. Chúng được thiết kế dành cho tương tác giữa con người và máy tính để người dùng có thể hiểu được nội dung. Nếu xem về phía tương tác giữa máy với nhau (ứng dụng với nhau) nó không có tác dụng bởi vì một máy không cần nhìn đẹp mà cần các phản hồi có cấu trúc như XML, JSON, CSV, vv.. và đó là web service được hình thành. Trong bài viết này sẽ trình bay về RESTful web APIs, ORM và demo tạo ứng dụng web api sử dụng ORM trong Golang.
RESTful Web APIs
Bản chất của kiến trúc REST tạo bởi một client và một server. REST API cho tạo điều kiện linh hoạt các hệ thống kết nối và gửi/nhận dữ liệu theo một cách trực tiếp. Bên server nhận bản tin đến và trả lời trong khi client tạo kết nối nhận bản tin gửi lại từ server.
RESTful client là một HTTP client và RESTful server là HTTP server. Mỗi và tất cả việc gọi REST API có một mối quan hệ giữa một HTTP verb và URL. Dữ liệu hoặc business logic trong database của một ứng dụng có thể được xác định bởi một API endpoint trong REST.
Object-realtional mapping - ORM
Trong phần mềm máy tính là một kỹ thuật lập trình để biến đổi dữ liệu giữ các hệ thống không tương thích trong các cơ sở dữ liệu quan hệ và các ngôn ngữ lập trình hướng đối tượng. Mục đích để một cơ sở dữ liệu đối tượng ảo có thể sử dụng từ bên trong ngôn ngữ lập trình.
GORM
Dự trên khái niệm của ORM thì GORM là một thư viện ORM dành cho Golang để các nhà phát triển dễ dàng làm việc trong việc lập trình. Các tính năng GORM gồm có
- Đầy đủ tính năng của ORM
- Associations (Has One, Has Many, Belongs To, Many To Many, Polymorphism, Single-table inheritance)
- Hooks (Before/After Create/Save/Update/Delete/Find)
- Eager loading with Preload, Joins
- Transactions, Nested Transactions, Save Point, RollbackTo to Saved Point
- Context, Prepared Statment Mode, DryRun Mode
- Batch Insert, FindInBatches, Find/Create with Map, CRUD with SQL Expr and Context Valuer
- SQL Builder, Upsert, Locking, Optimizer/Index/Comment Hints, Named Argument, SubQuery
- Composite Primary Key, Indexes, Constraints
- Auto Migrations
- Logger
- Extendable, flexible plugin API: Database Resolver (Multiple Databases, Read/Write Splitting) / Prometheus…
- Every feature comes with tests
Ứng dụng
Trước tiền để khởi tạo một ứng dụng trong Golang bạn cần tham khảo các phần sau
- Download and install Go
- How to Write Go Code
- Golang package management and module systems
- Echo Go web framework
Cấu trúc trong thư mục project
Tạo một thư mục và cd <PROJECT>
├── bin
│ └── app
├── cmd
│ └── server
│ └── main.go
├── db
│ └── adapter.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── handler
│ ├── server.go
│ ├── subject.go
├── models
│ ├── subject.go
.env
Để dễ dàng bắt đầu cần một số package sau
go mod init github.com/todo_app/server
go get -u github.com/labstack/echo/v4 v4.1.16
go get -u github.com/go-kit/kit v0.10.0
go get -u github.com/jinzhu/gorm v1.9.16
Sau khi đã có package tiện hơn nữa sẽ dùng docker image đã build sẵn
# docker-compose.yml
version: "3.5"
services:
app:
image: kimhuorlim/golang:1.14.4-alpine3.12
command: watch
volumes:
- .:/app
- go-mod:/go/pkg/mod
- build-cache:/root/.cache/go-build
- ~/.ssh/id_rsa:/root/.ssh/id_rsa
container_name: gorm_server
ports:
- 8080:8080
stdin_open: true
tty: true
env_file:
- .env
mysql:
image: mysql:5.7
container_name: gorm_db
environment:
MYSQL_DATABASE: gorm_todo
MYSQL_USER: root
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
restart: always
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
env_file:
- .env
volumes:
go-mod:
build-cache:
mysql_data:
Tiếp đến là start một ứng dụng web trong Golang
#cmd/server/main.go
package main
import (
"flag"
"net/http"
"os"
"time"
"github.com/todo_app/server/handler"
"github.com/go-kit/kit/log"
)
var (
fs = flag.NewFlagSet("todo_app", flag.ExitOnError)
httpAddr = fs.String("http-addr", ":8080", "HTTP server address")
)
func main() {
logger := log.NewJSONLogger(os.Stdout)
logger = log.WithPrefix(logger, "ts", log.DefaultTimestamp)
if err := fs.Parse(os.Args[1:]); err != nil {
logger.Log("binding", "flag", "err", err)
os.Exit(1)
}
server := &http.Server {
Handler: handler.NewHandler(),
Addr: *httpAddr,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
logger.Log("http", "server", "addr", *httpAddr)
if err := server.ListenAndServe(); err != nil {
logger.Log("http", "server", "err", err)
os.Exit(1)
}
}
# handler/server.go
package handler
import (
echo "github.com/labstack/echo/v4"
"net/http"
)
func NewHandler() http.Handler {
e := echo.New()
return e
}
Khi start ứng dụng sẽ có trang như sau
Kết nối DB
Với GORM hỗ trợ các database sau MySQL, PostgreSQL, SQlite, SQL Server, trong bài viết này sẽ làm việc với MySQL. Chi tiết về config kết nối hãy tham khảo thêm connecting_to_the_database.
#cmd/server/main.go
var (
...
dbConfig = fs.String("mysql", os.Getenv("GORM_DIALECT"), "mysql db connection")
)
func main() {
...
logger.Log("mysql", *dbConfig)
db, err := db.Connect()
if err != nil {
logger.Log("mysql", "err", err)
os.Exit(1)
}
defer db.Close()
server := &http.Server {
Handler: handler.NewHandler(db),
Addr: *httpAddr,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
...
# handler/server.go
...
type Server struct {
db *gorm.DB
}
func NewHandler(db *gorm.DB) http.Handler {
e := echo.New()
s := &Server{db: db}
...
}
# db/adapter.go
package db
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"os"
)
var (
dbDSN = "root:password@tcp(gorm_db)/gorm_todo?charset=utf8mb4&parseTime=True&loc=Local"
)
func Connect() (db *gorm.DB, err error) {
db, err = gorm.Open("mysql", os.Getenv("GORM_DIALECT"))
return db, err
}
Các bạn có thể tìm hiểu thêm về việc quản lý migration ở bài viết này Command line with grift in Go
Ở đây mình chạy bằng manual để tạo schema trong mysql với cmd sau
# mysql -uroot -ppassword
mysql> create database gorm_todo;
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| gorm_todo |
| mysql |
| performance_schema |
| sys |
+--------------------+
Khi restart lại hãy xem log sau có nghĩa là đã kết nối được đến DB.
gorm_db | 2020-09-18T15:24:58.784158Z 0 [Note] Event Scheduler: Loaded 0 events
gorm_db | 2020-09-18T15:24:58.785910Z 0 [Note] mysqld: ready for connections.
gorm_server | {"mysql":"root:password@tcp(gorm_db)/gorm_todo?charset=utf8mb4&parseTime=True&loc=Local","ts":"2020-09-18T15:24:59.948351675Z"}
gorm_server | {"addr":":8080","http":"server","ts":"2020-09-18T15:24:59.963873306Z"}
Tạo API endpoints
Các API endpoint gồm có
- Get all subjects
GET
/subjects
- Create subject
POST
/subjects
- Get subject
GET
/subjects/:id
- Update subject
PUT
/subjects/:id
- Delete subject
DELETE
/subjects/:id
Để map đến bảng subjects
trong mysql cần tạo một struct Subject
# models/subject.go
package models
import (
"github.com/jinzhu/gorm"
"time"
)
type Subject struct {
gorm.Model
Title string `json:"title"`
DueAt time.Time `json:"due_at"`
}
Migrate object Subject
với mysql
func main() {
...
defer db.Close()
db.AutoMigrate(&models.Subject{})
...
}
Tiếp đến trong handler/server.go
cần khai báo những endpoint trên.
...
s := &Server{db: db}
e.GET("/subjects", s.getAllSubjects)
e.POST("/subjects", s.createSubject)
e.GET("/subjects/:id", s.getSubject)
e.PUT("/subjects/:id", s.updateSubject)
e.DELETE("/subjects/:id", s.deleteSubject)
...
Define những logic cho mỗi endpoint
# handler/subject.go
package handler
import (
"github.com/jinzhu/gorm"
"github.com/todo_app/server/models"
"github.com/labstack/echo/v4"
"net/http"
)
func (s Server) getAllSubjects(c echo.Context) (err error) {
subjects := []models.Subject{}
s.db.Find(&subjects)
return c.JSONPretty(http.StatusOK, &subjects, " ")
}
func (s Server) createSubject(c echo.Context) (err error) {
subject := new(models.Subject)
if err = c.Bind(subject); err != nil {
return
}
s.db.Create(&subject)
return c.JSONPretty(http.StatusOK, subject, " ")
}
func (s Server) getSubject(c echo.Context) (err error) {
subject, err := getSubjectOr404(s.db, c.Param("id"))
if subject == nil {
return
}
return c.JSONPretty(http.StatusOK, &subject, " ")
}
func (s Server) updateSubject(c echo.Context) (err error) {
subject, err := getSubjectOr404(s.db, c.Param("id"))
if subject == nil {
return
}
if err = c.Bind(subject); err != nil {
return
}
if err = s.db.Model(&subject).Updates(&subject).Error; err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSONPretty(http.StatusOK, &subject, " ")
}
func (s Server) deleteSubject(c echo.Context) (err error) {
id := c.Param("id")
subject, err := getSubjectOr404(s.db, id)
if subject == nil {
return
}
if err = s.db.Delete(&models.Subject{}, id).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusNoContent, nil)
}
func getSubjectOr404(db *gorm.DB, id string) (*models.Subject, *echo.HTTPError) {
s := &models.Subject{}
if err := db.First(&s, id).Error; err != nil {
return nil, echo.NewHTTPError(http.StatusNotFound, err.Error())
}
return s, nil
}
Demo
Create subject
Get subject
- Khi gọi API với id không tồn tại
- Khi gọi API với id tồn tại trong DB
Get all subjects
Update subject
- Khi gọi API với id không tồn tại
- Khi gọi API với id tồn tại trong DB
Delete subject
- Khi gọi API với id không tồn tại
- Khi gọi API với id tồn tại trong DB
Kết luận
Trong bài viết đã trình bày về khái niệm cơ bản và một ứng dụng web API nhỏ với Golang và hy vọng có hữu ích cho bạn đọc.
Cảm ơn bạn đã đọc bài viết!