สร้าง Go Backend REST API
database ( ฐานข้อมูล ) คือ กลุ่มของข้อมูลที่ถูกรวบรวมเก็บไว้ โดยข้อมูลมีความสัมพันธ์ซึ่งกันและกัน เวลาที่แอปพลิเคชันหรือเว็บไซต์ต้องการจะนำข้อมูลมาประมวลผลหรือแสดงผล ก็จะนำข้อมูลมาจาก database
ข้อกำหนดเบื้องต้น
ข้อกำหนดสำหรับบทความนี้คือ คุณต้องติดตั้ง Go และ ปฏิบัติตามบทความ Models (โมเดล) มาก่อน
ติดตั้ง PostgreSQL
เริ่มเเรกดาวน์โหลด PostgreSQL มาก่อน สามารถดาวน์โหลดและติดตั้งโปรแกรมที่ : https://www.enterprisedb.com/downloads/postgres-postgresql-downloads ตั้งชื่อ dbname , password และ port ตามต้องการ (ในบทความ dbname คือ go_movies , password คือ lungmaker และ port คือ 54321)
> ลิงค์การติดตั้ง PostgreSQL
PostgreSQL เป็นระบบจัดการฐานข้อมูลโอเพ่นซอร์สระดับองค์กรที่ทันสมัย ที่พัฒนาโดย PostgreSQL Global Development Group เป็นระบบฐานข้อมูล SQL (Structured Query Language) เชิงวัตถุสัมพันธ์ที่มีประสิทธิภาพและขยายได้สูง
ตัวอย่างการใช้งาน https://pkg.go.dev/github.com/lib/pq
pgAdmin 4
pgAdmin 4 เป็นโปรแกรมที่ใช้ในการจัดการระบบฐานข้อมูลและการพัฒนา Open Source ที่หลากหลาย สำหรับ PostgreSQL
สร้าง database ชื่อ go_movies โดย คลิกขวาที่ Database -> Create -> Database…
ชื่อ go_movies -> Save
สร้างตารางใหม่ในฐานข้อมูล โดย คลิกขวาที่ Tables -> Query Tool
สร้างตารางใหม่ ด้วยคำสั่ง
CREATE TABLE public.movies (
id integer NOT NULL,
title character varying,
description text,
year integer,
release_date date,
runtime integer,
rating integer,
mpaa_rating character varying,
created_at timestamp without time zone,
updated_at timestamp without time zone
);
คลิกที่ รูป 3 เหลี่ยม (Execute)
แสดงข้อความว่า สร้างสำเร็จ
ที่ไฟล์ database -> Refresh…
จะพบ ตาราง movies เพิ่มเข้ามา คลิกขวา ที่ id -> Properties…
เลือก Constraints , Default เป็น nextval(‘movies_id_seq’::regclass) -> Save
จะพบ ตาราง movies เพิ่มเข้ามา คลิกขวา -> Query Tool
เพิ่มข้อมูล ที่ 1 ด้วยคำสั่ง
INSERT INTO movies
VALUES ( 1, 'The Shawshank Redemption', 'Two imprisoned men bond over a number of years','1994','1994-10-14','142','5','R','2021-05-17 00:00:00','2021-05-17 00:00:00');
คลิกที่ รูป 3 เหลี่ยม (Execute)
และเพิ่มข้อมูลที่ 2 – 4
INSERT INTO movies
VALUES ( 2, 'The Godfather', 'The aging patriarch of an organized crime dynasty transfers control to his son','1972','1972-03-24','175','5','R','2021-05-17 00:00:00','2021-05-17 00:00:00');
INSERT INTO movies
VALUES ( 3, 'The Dark Knight', 'The menace known as the Joker wreaks havoc on Gotham City','2008','2008-07-18','152','5','PG13','2021-05-17 00:00:00','2021-05-17 00:00:00');
INSERT INTO movies
VALUES ( 4, 'American Psycho', 'A wealthy New York investment banking executive hides his alternate psychopathic ego','2000','2000-04-14','102','4','R','2021-05-17 00:00:00','2021-05-17 00:00:00');
ดูข้อมูลที่เพิ่มเข้าไป movies -> View/Edit Data -> All Rows
pgAdmin 4 แสดงข้อมูลที่เพิ่มเข้าไป และตอนนี้ฐานข้อมูลของเราก็พร้อมที่จะทำงานในขั้นตอนต่อไป
ติดตั้ง github.com/lib/pq
กลับไปที่ VSCode ณ. โฟลเดอร์โมดูล backend-app ใช้คำสั่ง
go get -u github.com/lib/pq@v1.10.0
ทดสอบเชื่อมต่อกับฐานข้อมูล
ที่ไฟล์ main.go เขียนโค้ดดังนี้
package main
import (
"context"
"database/sql"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
_ "github.com/lib/pq"
)
const version = "1.0.0"
type config struct {
port int
env string
db struct {
dsn string
}
}
type AppStatus struct {
Status string `json:"status"`
Environment string `json:"environment"`
Version string `json:"version"`
}
type application struct {
config config
logger *log.Logger
}
func main() {
var cfg config
flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on")
flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production")
flag.StringVar(&cfg.db.dsn, "dsn", "postgres://postgres:lungmaker@localhost:54321/go_movies?sslmode=disable", "Postgres connection string")
flag.Parse()
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)
db, err := openDB(cfg)
if err != nil {
logger.Fatal(err)
}
defer db.Close()
app := &application{
config: cfg,
logger: logger,
}
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.port),
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
logger.Println("Starting server on port", cfg.port)
err = srv.ListenAndServe()
if err != nil {
log.Println(err)
}
}
func openDB(cfg config) (*sql.DB, error) {
db, err := sql.Open("postgres", cfg.db.dsn)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.PingContext(ctx)
if err != nil {
return nil, err
}
return db, nil
}
ทดสอบการทำงานด้วยคำสั่ง
go run ./cmd/api/ .
ผลลัพธ์การทำงาน : ถ้าไม่แสดง error และขึ้นข้อความ Starting server on port 4000 แสดงว่าการเชื่อมต่อกับฐานข้อมูลสำเร็จแล้ว
Database functions
ที่ไฟล์ models.go เขียนโค้ดดังนี้
package models
import (
"database/sql"
"time"
)
// Models is the wrapper for database
type Models struct {
DB DBModel
}
// NewModels returns models with db pool
func NewModels(db *sql.DB) Models {
return Models {
DB: DBModel{DB: db},
}
}
// Movie is the type for movies
type Movie struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Year int `json:"year"`
ReleaseDate time.Time `json:"release_date"`
Runtime int `json:"runtime"`
Rating int `json:"rating"`
MPAARating string `json:"mpaa_rating"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
MovieGenre []MovieGenre `json:"-"`
}
// Genre is the type for genre
type Genre struct {
ID int `json:"id"`
GenreName string `json:"genre_name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// MovieGenre is the type for movie genre
type MovieGenre struct {
ID int `json:"id"`
MovieID int `json:"movie_id"`
GenreID int `json:"genre_id"`
Genre Genre `json:"genre"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
สร้างไฟล์ใหม่ในโฟลเดอร์ models ชื่อ movies-db.go เขียนโค้ดดังนี้
package models
import (
"context"
"database/sql"
"time"
)
type DBModel struct {
DB *sql.DB
}
// Get returns one movie and error, if any
func (m *DBModel) Get(id int) (*Movie, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `select id, title, description, year, release_date, rating, runtime, mpaa_rating,
created_at, updated_at from movies where id = $1
`
row := m.DB.QueryRowContext(ctx, query, id)
var movie Movie
err := row.Scan(
&movie.ID,
&movie.Title,
&movie.Description,
&movie.Year,
&movie.ReleaseDate,
&movie.Rating,
&movie.Runtime,
&movie.MPAARating,
&movie.CreatedAt,
&movie.UpdatedAt,
)
if err != nil {
return nil, err
}
return &movie, nil
}
// All returns all movies and error, if any
func (m *DBModel) All(id int) ([]*Movie, error) {
return nil, nil
}
ที่ไฟล์ movie-handlers.go เขียนโค้ดดังนี้
package main
import (
"errors"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.Atoi(params.ByName("id"))
if err != nil {
app.logger.Print(errors.New("invalid id parameter"))
app.errorJSON(w, err)
return
}
app.logger.Println("id is", id)
movie, err := app.models.DB.Get(id)
// movie := models.Movie {
// ID: id,
// Title: "Some movie",
// Description: "Some description",
// Year: 2021,
// ReleaseDate: time.Date(2021, 01, 01, 01, 0, 0, 0, time.Local),
// Runtime: 100,
// Rating: 5,
// MPAARating: "PG-13",
// CreatedAt: time.Now(),
// UpdatedAt: time.Now(),
// }
err = app.writeJSON(w, http.StatusOK, movie, "movie")
}
func (app *application) getAllMovies(w http.ResponseWriter, r *http.Request) {
}
ที่ไฟล์ main.go เขียนโค้ดดังนี้
package main
import (
"backend/models"
"context"
"database/sql"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
_ "github.com/lib/pq"
)
const version = "1.0.0"
type config struct {
port int
env string
db struct {
dsn string
}
}
type AppStatus struct {
Status string `json:"status"`
Environment string `json:"environment"`
Version string `json:"version"`
}
type application struct {
config config
logger *log.Logger
models models.Models
}
func main() {
var cfg config
flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on")
flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production")
flag.StringVar(&cfg.db.dsn, "dsn", "postgres://postgres:lungmaker@localhost:54321/go_movies?sslmode=disable", "Postgres connection string")
flag.Parse()
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)
db, err := openDB(cfg)
if err != nil {
logger.Fatal(err)
}
defer db.Close()
app := &application{
config: cfg,
logger: logger,
models: models.NewModels(db),
}
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.port),
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
logger.Println("Starting server on port", cfg.port)
err = srv.ListenAndServe()
if err != nil {
log.Println(err)
}
}
func openDB(cfg config) (*sql.DB, error) {
db, err := sql.Open("postgres", cfg.db.dsn)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.PingContext(ctx)
if err != nil {
return nil, err
}
return db, nil
}
ทดสอบการทำงาน
ไปที่เว็บเบราว์เซอร์ป้อน url เป็น http://localhost:4000/v1/movie/1 เพจจะแสดงข้อมูลตามฐานข้อมูลของเรา
ทดสอบเป็น http://localhost:4000/v1/movie/2
ทดสอบเป็น http://localhost:4000/v1/movie/5
Solution to challenge
ที่ pgAdmin 4 สร้างตารางใหม่ในฐานข้อมูล โดย คลิกขวาที่ Tables -> Query Tool สร้างตารางใหม่ ด้วยคำสั่ง
CREATE TABLE public.movies_genres (
id integer NOT NULL,
movie_id integer,
genre_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone
);
และ
CREATE SEQUENCE public.movies_genres_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE SEQUENCE public.movies_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE public.genres (
id integer NOT NULL,
genre_name character varying,
created_at timestamp without time zone,
updated_at timestamp without time zone
);
CREATE SEQUENCE public.genres_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ไฟล์ movies-db.go เขียนโค้ดดังนี้
package models
import (
"context"
"database/sql"
"time"
)
type DBModel struct {
DB *sql.DB
}
// Get returns one movie and error, if any
func (m *DBModel) Get(id int) (*Movie, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `select id, title, description, year, release_date, rating, runtime, mpaa_rating,
created_at, updated_at from movies where id = $1
`
row := m.DB.QueryRowContext(ctx, query, id)
var movie Movie
err := row.Scan(
&movie.ID,
&movie.Title,
&movie.Description,
&movie.Year,
&movie.ReleaseDate,
&movie.Rating,
&movie.Runtime,
&movie.MPAARating,
&movie.CreatedAt,
&movie.UpdatedAt,
)
if err != nil {
return nil, err
}
// get genres, if any
query = `select
mg.id, mg.movie_id, mg.genre_id, g.genre_name
from
movies_genres mg
left join genres g on (g.id = mg.genre_id)
where
mg.movie_id = $1
`
rows, _ := m.DB.QueryContext(ctx, query, id)
defer rows.Close()
genres := make(map[int]string)
for rows.Next() {
var mg MovieGenre
err := rows.Scan(
&mg.ID,
&mg.MovieID,
&mg.GenreID,
&mg.Genre.GenreName,
)
if err != nil {
return nil, err
}
genres[mg.ID] = mg.Genre.GenreName
}
movie.MovieGenre = genres
return &movie, nil
}
// All returns all movies and error, if any
func (m *DBModel) All(id int) ([]*Movie, error) {
return nil, nil
}
ที่ไฟล์ models.go เขียนโค้ดดังนี้
package models
import (
"database/sql"
"time"
)
// Models is the wrapper for database
type Models struct {
DB DBModel
}
// NewModels returns models with db pool
func NewModels(db *sql.DB) Models {
return Models{
DB: DBModel{DB: db},
}
}
// Movie is the type for movies
type Movie struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Year int `json:"year"`
ReleaseDate time.Time `json:"release_date"`
Runtime int `json:"runtime"`
Rating int `json:"rating"`
MPAARating string `json:"mpaa_rating"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
MovieGenre map[int]string `json:"genres"`
}
// Genre is the type for genre
type Genre struct {
ID int `json:"-"`
GenreName string `json:"genre_name"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
// MovieGenre is the type for movie genre
type MovieGenre struct {
ID int `json:"-"`
MovieID int `json:"-"`
GenreID int `json:"-"`
Genre Genre `json:"genre"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
ไปที่เว็บเบราว์เซอร์ป้อน url เป็น http://localhost:4000/v1/movie/1
Getting all movies as JSON
ไฟล์ movies-db.go เขียนโค้ดดังนี้
package models
import (
"context"
"database/sql"
"time"
)
type DBModel struct {
DB *sql.DB
}
// Get returns one movie and error, if any
func (m *DBModel) Get(id int) (*Movie, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `select id, title, description, year, release_date, rating, runtime, mpaa_rating,
created_at, updated_at from movies where id = $1
`
row := m.DB.QueryRowContext(ctx, query, id)
var movie Movie
err := row.Scan(
&movie.ID,
&movie.Title,
&movie.Description,
&movie.Year,
&movie.ReleaseDate,
&movie.Rating,
&movie.Runtime,
&movie.MPAARating,
&movie.CreatedAt,
&movie.UpdatedAt,
)
if err != nil {
return nil, err
}
// get genres, if any
query = `select
mg.id, mg.movie_id, mg.genre_id, g.genre_name
from
movies_genres mg
left join genres g on (g.id = mg.genre_id)
where
mg.movie_id = $1
`
rows, _ := m.DB.QueryContext(ctx, query, id)
defer rows.Close()
genres := make(map[int]string)
for rows.Next() {
var mg MovieGenre
err := rows.Scan(
&mg.ID,
&mg.MovieID,
&mg.GenreID,
&mg.Genre.GenreName,
)
if err != nil {
return nil, err
}
genres[mg.ID] = mg.Genre.GenreName
}
movie.MovieGenre = genres
return &movie, nil
}
// All returns all movies and error, if any
func (m *DBModel) All() ([]*Movie, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `select id, title, description, year, release_date, rating, runtime, mpaa_rating,
created_at, updated_at from movies order by title
`
rows, err := m.DB.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var movies []*Movie
for rows.Next() {
var movie Movie
err := rows.Scan(
&movie.ID,
&movie.Title,
&movie.Description,
&movie.Year,
&movie.ReleaseDate,
&movie.Rating,
&movie.Runtime,
&movie.MPAARating,
&movie.CreatedAt,
&movie.UpdatedAt,
)
if err != nil {
return nil, err
}
// get genres, if any
genreQuery := `select
mg.id, mg.movie_id, mg.genre_id, g.genre_name
from
movies_genres mg
left join genres g on (g.id = mg.genre_id)
where
mg.movie_id = $1
`
genreRows, _ := m.DB.QueryContext(ctx, genreQuery, movie.ID)
genres := make(map[int]string)
for genreRows.Next() {
var mg MovieGenre
err := genreRows.Scan(
&mg.ID,
&mg.MovieID,
&mg.GenreID,
&mg.Genre.GenreName,
)
if err != nil {
return nil, err
}
genres[mg.ID] = mg.Genre.GenreName
}
genreRows.Close()
movie.MovieGenre = genres
movies = append(movies, &movie)
}
return movies, nil
}
ไฟล์ movie-handlers.go เขียนโค้ดดังนี้
package main
import (
"errors"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.Atoi(params.ByName("id"))
if err != nil {
app.logger.Print(errors.New("invalid id parameter"))
app.errorJSON(w, err)
return
}
movie, err := app.models.DB.Get(id)
err = app.writeJSON(w, http.StatusOK, movie, "movie")
if err != nil {
app.errorJSON(w, err)
return
}
}
func (app *application) getAllMovies(w http.ResponseWriter, r *http.Request) {
movies, err := app.models.DB.All()
if err != nil {
app.errorJSON(w, err)
return
}
err = app.writeJSON(w, http.StatusOK, movies, "movies")
if err != nil {
app.errorJSON(w, err)
return
}
}
func (app *application) deleteMovie(w http.ResponseWriter, r *http.Request) {
}
func (app *application) insertMovie(w http.ResponseWriter, r *http.Request) {
}
func (app *application) updateMovie(w http.ResponseWriter, r *http.Request) {
}
func (app *application) searchMovies(w http.ResponseWriter, r *http.Request) {
}
ที่เว็บเบราว์เซอร์ป้อน url เป็น http://localhost:4000/v1/movies
CORS middleware
CORS ย่อมาจาก Cross-Origin Resource Sharing
Middleware คือ software computer ที่คอยช่วยเหลือดูแล application ที่รันอยู่บน OS หรือจะเรียกว่าตัวเชื่อมระหว่าง APP และ OS ก็ได้ ซึงมันช่วยให้ developer สามารถเชื่อมต่อสื่อสารกับภายนอกได้ง่ายขึ้น ทำให้ลดภาระในการดูแลรายละเอียดรอบข้างและเน้นแต่งานหลักที่ต้องการได้
Middleware เอาไปใช้ทำอะไร
– เชื่อมต่อ application ระหว่าง network
– กรองข้อมูลเพื่อให้มีเหลือแต่เฉพาะงานที่จำเป็น หรือ ช่วยส่งข้อมูลที่ต้องการ privacy protection
– สร้างความเสถียรถาพ ให้ระบบพร้อมรองรับข้อมูลตลอดเวลา
สร้างไฟล์ใหม่ในโฟลเดอร์ api ชื่อ middleware.go เขียนโค้ดดังนี้
package main
import "net/http"
func (app *application) enableCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r)
})
}
ที่ไฟล์ routes.go เขียนโค้ดดังนี้
package main
import (
"net/http"
"github.com/julienschmidt/httprouter"
)
func (app *application) routes() http.Handler {
router := httprouter.New()
router.HandlerFunc(http.MethodGet, "/status", app.statusHandler)
router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie)
router.HandlerFunc(http.MethodGet, "/v1/movies", app.getAllMovies)
return app.enableCORS(router)
}
สั่งให้ทำงาน เพื่อรอการเชื่อมต่อจาก Front-End
credit : https://www.udemy.com/course/working-with-react-and-go-golang/