Data Models
Defining models and establishing database connection
This section holds significant importance as we'll be implementing major components such as database connections and creating new GORM models.
Database Connection
Let's first create a database connection.
But before that you need to create
additional directories under the core
.
abstract
- Contains interface, part of the interaction layer in a database-oriented application, often in combination with an ORM library like GORM.controllers
- Stores all the HTTP handlers and routes.database
- Initializing db connection and GORM models.util.go
- re-usable code shared across the application.
Create db.go
file under database
directory.
db.go
package database
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"log"
"os"
"strconv"
)
type DBClient interface {
DBMigrate() error
}
type Client struct {
db *gorm.DB
}
func NewClient() (DBClient, error) {
databaseHost := os.Getenv("DB_HOST")
databaseUsername := os.Getenv("DB_USERNAME")
databasePassword := os.Getenv("DB_PASSWORD")
databaseName := os.Getenv("DB_NAME")
databasePort := os.Getenv("DB_PORT")
dbPort, err := strconv.Atoi(databasePort)
if err != nil {
log.Fatal("Invalid DB Port")
}
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s",
databaseHost, databaseUsername, databasePassword, databaseName, dbPort, "disable")
dbInfo, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
client := Client{db: dbInfo}
return client, nil
}
func (c Client) DBMigrate() error {
return nil
}
func (c Client) CloseDBConnection() {
db, err := c.db.DB()
if err != nil {
panic("Failed to close connection from database")
}
db.Close()
}
type DBClient
interface - This interface defines what methods aDBClient
should have. In this case, it isDBMigrate()
, which is a method for migrating the database.type Client
struct - This struct contains a pointer to a GORM DB object. This is the main object that the application will use to interact with the database.func NewClient() (DBClient, error)
- This function initializes a new Client object. It reads environment variables to get the database connection details (host
,username
,password
,database name
, andport
). It then uses these details to open a new connection to the postgres database. If successful, it returns a client that implements theDBClient
interface (since Client struct implementsDBMigrate()
method from DBClient interface). If not successful, it returns an error.func (c Client) DBMigrate() error
- In this function, it just returns nil signifying no error. However, in a real application, you would put the code to migrate your database here. We will come back to this function later for the implementation.func (c Client) CloseDBConnection()
- This method is used to close the db connection.
Next, make sure to update the environment variables.
databaseHost := os.Getenv("DB_HOST")
databaseUsername := os.Getenv("DB_USERNAME")
databasePassword := os.Getenv("DB_PASSWORD")
databaseName := os.Getenv("DB_NAME")
databasePort := os.Getenv("DB_PORT")
Now, create main.go
under project root.
The main()
function is the program entry point. We are going to initialize the database, migrate db tables etc.
package main
import (
_ "github.com/joho/godotenv/autoload"
"go-gin-bookstore/core/database"
"log"
)
func main() {
// establish connection with db
db, err := database.NewClient()
if err != nil {
panic("Something wrong with DBClient")
}
err = db.DBMigrate()
if err != nil {
log.Fatal("Database Migration Failed!")
return
}
}
Once our models are prepared, we'll proceed to implement db.DBMigrate()
. This will occur in HTTP Handler section.
Defining Models
Book Model
Let's begin by creating our Book
model.
models/book.go
package models
import (
"gorm.io/gorm"
"time"
)
// DateParser interface wraps the ParsePublicationDate method
type DateParser interface {
ParsePublicationDate() (time.Time, error)
}
type Book struct {
gorm.Model
Title string `json:"title" binding:"required"`
ISBN string `json:"isbn" binding:"required"`
Image string `json:"image,omitempty"`
PublicationDate time.Time `json:"publication_date" binding:"required"`
}
type BookParams struct {
Id int64 `json:"id"`
Title string `json:"title"`
ISBN string `json:"isbn"`
PublicationDate string `json:"publication_date"`
}
// ParsePublicationDate Implementing the DateParser interface
func (params BookParams) ParsePublicationDate() (time.Time, error) {
return ValidateDate(params.PublicationDate)
}
type UpdateBookParams struct {
Title string `json:"title" binding:"required"`
ISBN string `json:"isbn" binding:"required"`
PublicationDate string `json:"publication_date" binding:"required"`
}
// ParsePublicationDate Implementing the DateParser interface
func (params UpdateBookParams) ParsePublicationDate() (time.Time, error) {
return ValidateDate(params.PublicationDate)
}
func ValidateDate(pubDate string) (time.Time, error) {
standardFormat := "2006-01-02" // This layout represents the date format "YYYY-MM-DD"
date, err := time.Parse(standardFormat, pubDate)
if err != nil {
return time.Time{}, err
}
return date, nil
}
In this Go struct, we have the following fields:
gorm.Model
- this is an embedded field. It means that the Book struct includes all the fields defined in GORM.Title
- This is a string field representing the title of a book.ISBN
- This is another string field which stands for International Standard Book Number.Image
- This is a string that might contain a link or path to the image of the book cover.PublicationDate
- This is of typetime.Time
, storing the book's publication date.
Each field is tagged with json:"<name>"
, indicating how the field should be marshalled/unmarshalled when encoding/decoding JSON. So, when this struct is encoded into JSON, the field Title
will be represented as "title"
in the JSON object. Similarly, when decoding JSON data into this struct, the decoder will expect a field named "title"
and will assign its value to the Title
field of the struct.
type BookParams struct {
Id int64 `json:"id"`
Title string `json:"title"`
ISBN string `json:"isbn"`
PublicationDate string `json:"publication_date"`
}
The following structs will be used specifically for creating/updating book.
type BookParams struct {
Id int64 `json:"id"`
Title string `json:"title"`
ISBN string `json:"isbn"`
PublicationDate string `json:"publication_date"`
}
type UpdateBookParams struct {
Title string `json:"title" binding:"required"`
ISBN string `json:"isbn" binding:"required"`
PublicationDate string `json:"publication_date" binding:"required"`
}
This is an interface that declares the ParsePublicationDate
method. Any type that defines this method is said to satisfy the DateParser
interface. This method returns a time.Time
type and an error.
type DateParser interface {
ParsePublicationDate() (time.Time, error)
}
ParsePublicationDate
: This function, defined for bothBookParams
andUpdateBookParams
, uses theValidateDate
function to attempt to parse thePublicationDate
string property and convert it to atime.Time
type.ValidateDate
: This function takes a string argument representing a date and attempts to parse it into atime.Time
type using the standard date format "2006-01-02" (which represents "YYYY-MM-DD"). If successful, the parsed date and a nil error are returned; if unsuccessful, the zero value fortime.Time
and the error are returned.
// ParsePublicationDate Implementing the DateParser interface
func (params UpdateBookParams) ParsePublicationDate() (time.Time, error) {
return ValidateDate(params.PublicationDate)
}
func ValidateDate(pubDate string) (time.Time, error) {
standardFormat := "2006-01-02" // This layout represents the date format "YYYY-MM-DD"
date, err := time.Parse(standardFormat, pubDate)
if err != nil {
return time.Time{}, err
}
return date, nil
}
Author Model
- The
Name
fields represent the name of an author. - The Books field is of type
[]Book
, which suggests that an author can have multiple books associated with them. The taggorm:"many2many:author_books;"
specifies a many-to-many relationship between books and authors. - The
AuthorBook
struct: This is used to model the many-to-many relationship between authors and books. It includes fieldsAuthorID
andBookID
, and thebinding:"required"
tag indicates that these fields are mandatory.
package models
import "gorm.io/gorm"
type Author struct {
gorm.Model
Name string `json:"name" binding:"required"`
Books []Book `gorm:"many2many:author_books;"`
}
type AuthorBook struct {
AuthorID int64 `json:"author_id" binding:"required"`
BookID int64 `json:"book_id" binding:"required"`
}
Customer & Review Model
Customer
: Represents a Customer withFirstName
,LastName
,Email
,PhoneNumber
, andAddress
as properties. The struct tagbinding:"required"
indicates that these fields must be provided.Review
: Represents a review completed by a customer on a book. It includes the ID of the customer and the book, a rating, and a comment. There are also references to theCustomer
andBook
structures, creating a one-to-many relationship through the keys CustomerID and BookID.ReviewParams
: A helper struct to validate incoming requests data when creating a book review.CustomerParams
: A helper struct to validate incoming requests data when creating a new customer.ReviewList
: Listing all reviews.
The gorm:"foreignKey:CustomerID"
and gorm:"foreignKey:BookID"
tags in review struct inform the ORM about the relational mapping between the models. The json:"-"
tag means this field won't be serialized when the struct is converted to JSON.
customer.go
package models
import "gorm.io/gorm"
type Customer struct {
gorm.Model
FirstName string `json:"first_name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Email string `json:"email" binding:"required"`
PhoneNumber string `json:"phone_number" binding:"required"`
Address string `json:"address" binding:"required"`
}
type Review struct {
gorm.Model
CustomerID int64 `json:"customer_id" binding:"required"`
BookID int64 `json:"book_id" binding:"required"`
Rating int `json:"rating" binding:"required"`
Comment string `json:"comment,omitempty"`
Customer Customer `gorm:"foreignKey:CustomerID" json:"-"`
Book Book `gorm:"foreignKey:BookID" json:"-"`
}
type ReviewParams struct {
CustomerID int64 `json:"customer_id" binding:"required"`
BookID int64 `json:"book_id" binding:"required"`
Rating int `json:"rating" binding:"required" validate:"min=1,max=5"`
Comment string `json:"comment,omitempty" binding:"required"`
}
type CustomerParams struct {
FirstName string `json:"first_name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Address string `json:"address" binding:"required"`
}
type ReviewList struct {
Id int64 `json:"id"`
Rating int `json:"rating"`
Comment string `json:"comment"`
}
We are done with the models. The upcoming section will concentrate on implementing the interfaces.