Let's Go Further عملیات CRUD › راه‌اندازی مدل فیلم
قبلی · فهرست مطالب · بعدی
فصل ۷.۱.

راه‌اندازی مدل فیلم

در این فصل قرار است کد اسکلت مدل پایگاه داده فیلم خود را راه‌اندازی کنیم.

اگر اصطلاح مدل را دوست ندارید، می‌توانید آن را به‌عنوان لایهٔ دسترسی به داده یا ذخیره‌سازی در نظر بگیرید. اما هر نامی که ترجیح می‌دهید، اصل یکی است — تمام کد خواندن و نوشتن داده‌های فیلم در پایگاه دادهٔ PostgreSQL ما را کپسوله می‌کند.

بیایید به فایل internal/data/movies.go برگردیم و یک ساختار MovieModel و چند method جای‌گذار برای اجرای عملیات پایهٔ CRUD (ایجاد، خواندن، به‌روزرسانی و حذف) روی جدول movies پایگاه داده‌مان ایجاد کنیم.

فایل: internal/data/movies.go
package data

import (
    "database/sql" // New import
    "time"

    "greenlight.alexedwards.net/internal/validator"
)

...

// Define a MovieModel struct type which wraps a sql.DB connection pool.
type MovieModel struct {
    DB *sql.DB
}

// Add a placeholder method for inserting a new record in the movies table.
func (m MovieModel) Insert(movie *Movie) error {
    return nil
}

// Add a placeholder method for fetching a specific record from the movies table.
func (m MovieModel) Get(id int64) (*Movie, error) {
    return nil, nil
}

// Add a placeholder method for updating a specific record in the movies table.
func (m MovieModel) Update(movie *Movie) error {
    return nil
}

// Add a placeholder method for deleting a specific record from the movies table.
func (m MovieModel) Delete(id int64) error {
    return nil
}

به‌عنوان یک قدم اضافه، MovieModel خود را در یک ساختار والد Models قرار می‌دهیم. انجام این کار کاملاً اختیاری است، اما مزیتش این است که یک «container» راحت تکی به شما می‌دهد که می‌تواند تمام مدل‌های پایگاه دادهٔ شما را در حال رشد برنامه نگه دارد و نمایش دهد.

اگر همراه ما کدنویسی می‌کنید، فایل جدید internal/data/models.go بسازید و کد زیر را اضافه کنید:

$ touch internal/data/models.go
فایل: internal/data/models.go
package data

import (
    "database/sql"
    "errors"
)

// Define a custom ErrRecordNotFound error. We'll return this from our Get() method when
// looking up a movie that doesn't exist in our database.
var (
    ErrRecordNotFound = errors.New("record not found")
)

// Create a Models struct which wraps the MovieModel. We'll add other models to this,
// like a UserModel and PermissionModel, as our build progresses.
type Models struct {
    Movies MovieModel
}

// For ease of use, we also add a New() method which returns a Models struct containing
// the initialized MovieModel.
func NewModels(db *sql.DB) Models {
    return Models{
        Movies: MovieModel{DB: db},
    }
}

و حالا، بیایید فایل cmd/api/main.go خود را ویرایش کنیم تا ساختار Models در تابع main() ما مقداردهی اولیه شود و سپس به‌عنوان یک dependency به handlerهایمان پاس داده شود. به این شکل:

فایل: cmd/api/main.go
package main

import (
    "context"
    "database/sql"
    "flag"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "time"

    "greenlight.alexedwards.net/internal/data" // New import

    _ "github.com/lib/pq"
)

...

// Add a models field to hold our new Models struct.
type application struct {
    config config
    logger *slog.Logger
    models data.Models
}

func main() {
    var cfg config

    flag.IntVar(&cfg.port, "port", 4000, "API server port")
    flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")

    flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN")

    flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections")
    flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections")
    flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time")

    flag.Parse()

    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    db, err := openDB(cfg)
    if err != nil {
        logger.Error(err.Error())
        os.Exit(1)
    }

    defer db.Close()

    logger.Info("database connection pool established")

    // Use the data.NewModels() function to initialize a Models struct, passing in the 
    // connection pool as a parameter.
    app := &application{
        config: cfg,
        logger: logger,
        models: data.NewModels(db),
    }

    srv := &http.Server{
        Addr:         fmt.Sprintf(":%d", cfg.port),
        Handler:      app.routes(),
        IdleTimeout:  time.Minute,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        ErrorLog:     slog.NewLogLogger(logger.Handler(), slog.LevelError),
    }

    logger.Info("starting server", "addr", srv.Addr, "env", cfg.env)

    err = srv.ListenAndServe()
    logger.Error(err.Error())
    os.Exit(1)
}

...

اگر می‌خواهید، در این مرحله می‌توانید برنامه را ری‌استارت کنید. باید ببینید که کد کامپایل و با موفقیت اجرا می‌شود.

$ go run ./cmd/api
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

یکی از نقاط قوت این الگو این است که کد اجرای عملیات روی جدول movies از دیدگاه handlerهای API ما بسیار واضح و خوانا خواهد بود. برای مثال، می‌توانیم method Insert() را به‌سادگی با نوشتن این اجرا کنیم:

app.models.Movies.Insert(...)

ساختار کلی نیز به‌راحتی قابل گسترش است. وقتی در آینده مدل‌های پایگاه دادهٔ بیشتری ایجاد کنیم، کافی است آن‌ها را در ساختار Models قرار دهیم تا به‌طور خودکار در اختیار handlerهای API ما قرار بگیرند.