Programming

How to Communication Golang with MSSQL Server

Dependency

Make sure you have created a project with go mod init mssql-go in the mssql-go folder and the dependency that we will use is using

import _ "github.com/microsoft/go-mssqldb"

If it isn’t there, we can try downloading the dependency above first using the command below.

go get github.com/microsoft/go-mssqldb

Install MS SQL Server and create a database with the name recordings and also the table album for the needs of this project. If you don’t have SQL Server Database then we need to install it first. You can also use Docker for the installation.

If we use a Mackbook M1 we can also use the Docker below

docker run -e "ACCEPT_EULA=1" -e "MSSQL_SA_PASSWORD=MyPass@word" -e "MSSQL_PID=Developer" -e "MSSQL_USER=SA" -p 1433:1433 -d --name=sql mcr.microsoft.com/azure-sql-edge

Then, to make it easier to access the MS SQL database, you can install the [VS Code MS SQL Connection] plugin (https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql)

Database Schema

After the MSSQL installation is complete and we can connect to SQL Server, it’s time for us to continue by preparing the database and tables, following the schema that we will need.

CREATE schema dbo
GO
CREATE TABLE album(
  id INT IDENTITY(1,1) PRIMARY KEY,
  title NVARCHAR(255),
  artist NVARCHAR(255),
  price INT
)
GO

Create Database Connection Function

In creating this database connection function we will use a function from SQL which has been prepared by the go library. Let’s create this database connection function by returning return db connection.

func openDB(connString string, setLimits bool) (*sql.DB, error) {
	var err error
	db, err := sql.Open("sqlserver", connString)
	if err != nil {
		log.Fatal("Error creating connection pool: ", err.Error())
	}

	if setLimits {
		fmt.Println("setting limits")
		db.SetMaxIdleConns(5)
		db.SetMaxOpenConns(5)
	}

	ctx := context.Background()
	err = db.PingContext(ctx)
	if err != nil {
		log.Fatal(err.Error())
	}
	fmt.Printf("Connected!\n")
	return db, nil
}

In this connection function we use sql.Open to make a database connection where we also receive parameters from the main function dsn` which contains the location server, user, password and the destination database.

db, err := sql.Open("sqlserver", dsn)

Then we also provide some configuration to ensure this connection has a SetMaxOpenConns Connection when we run this service and we also set SetMaxIdleConns to ensure our connection is not misconfigured and runs normally.

db.SetMaxOpenConns(5)
db.SetMaxIdleConns(5)

Next, we also make sure to set a context timeout limit, which is useful here to ensure that the connection to this database doesn’t take too long. In this case, we set the maximum timeout limit to 5 seconds.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

And finally, we do PingContext to ensure that the connection to this database is not hampered by anything, for example delay or network.

err = db.PingContext(ctx)

Create Services and Queries to the Database

At this stage, we will create several functions to access data from the database. We also create functions with interface encapsulation. Let’s just run it and practice. Create a services folder then we also create a file in that folder with the file name contract.go.

package services

type Album struct {
	ID     int64
	Title  string
	Artist string
	Price  float32
}

type AlbumService interface {
	Get(id int64) (*Album, error)
	Create(album *Album) error
	GetAllAlbum() ([]Album, error)
	BatchCreate(albums []Album) error
	Update(Album Album) error
	Delete(id int64) error
}

We can see that the contract file contains struct data which will be used as the basis for the album data structure.

Meanwhile, the functions that we will use are the requirements for adding, changing, deleting. Below we create an interface which we will later implement these functions too.

type AlbumService interface {
	Get(id int64) (*Album, error)
	Create(album *Album) error
	GetAllAlbum() ([]Album, error)
	BatchCreate(albums []Album) error
	Update(Album Album) error
	Delete(id int64) error
}

Retrieve Album Data by Id

Let’s create a function that can later retrieve data from a SQL Server database. Before going to the related functions, we need to first define the existing functions by including the database connection that we defined at the beginning.

type MSSQLService struct {
	db *sql.DB
}

func NewMSSQLService(db *sql.DB) *MSSQLService {
	return &MSSQLService{db: db}
}

We use this initialization so that we can create a function that retrieves data into the database with an existing connection. Then next, let’s create a function to retrieve album data using the id parameter.

func (p *MSSQLService) Get(id int64) (*Album, error) {
	query := `
        SELECT id, title, artist, price
        FROM dbo.album
        WHERE id = @id`

	var album Album

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	err := p.db.QueryRowContext(ctx, query, sql.Named("id", id)).Scan(
		&album.ID,
		&album.Title,
		&album.Artist,
		&album.Price,
	)

	if err != nil {
		return nil, err
	}

	return &album, nil
}

In the function above we see creating a query into the database as below.

query := `
  SELECT id, title, artist, price
  FROM dbo.album
  WHERE id = @id`

Then we also make sure that the query needs to be timed out so that it doesn’t take too long to retrieve data into the database. In this program we use a timeout of 15 seconds.

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

Next, we also use the QueryRowContext command to retrieve the data after we initialize the connection to the database.

err := p.db.QueryRowContext(ctx, query, sql.Named("id", id)).Scan(
  &album.ID,
  &album.Title,
  &album.Artist,
  &album.Price,
)

The QueryRowContext command is used to retrieve data from the database by returning only one row of data. Next, if the data exists, we will save the data into the album variable.

Save Album Data to Database

Apart from retrieving data from the database, we also have to be able to send data from the system to the database. Below we will create a create function.

func (p *MSSQLService) Create(album *Album) error {
	query := `
        INSERT INTO dbo.album (title, artist, price) 
        VALUES (@title, @artist, @price)`

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	_, err := p.db.ExecContext(ctx, query,
		sql.Named("title", album.Title),
		sql.Named("artist", album.Artist),
		sql.Named("price", album.Price))
	if err != nil {
		return err
	}

	return nil
}

As usual, because this is a native query where we define our own query in the code, we need to define the query first. The following is a query to insert data into the database.

query := `
      INSERT INTO dbo.album (title, artist, price) 
      VALUES (@title, @artist, @price)`

Set a timeout to be more secure and then we execute create. Here we use the same query as retrieving data because we will retrieve the ID after it is added.

_, err := p.db.ExecContext(ctx, query,
		sql.Named("title", album.Title),
		sql.Named("artist", album.Artist),
		sql.Named("price", album.Price))

Retrieve All Data in Database

Let’s create the next function, which is to retrieve all data from the database as follows.

func (p *MSSQLService) GetAllAlbum() ([]Album, error) {
	query := `
		SELECT id, title, artist, price
		FROM dbo.album`

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	var albums []Album

	rows, err := p.db.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var album Album
		err := rows.Scan(
			&album.ID,
			&album.Title,
			&album.Artist,
			&album.Price,
		)
		if err != nil {
			return nil, err
		}

		albums = append(albums, album)
	}

	return albums, nil
}

In this function we will retrieve all the data and as we can see the query function used is different from the previous query, namely we use the query QueryContext where the results of this function return have multi rows so we need to loop to store the data in a struct array .

	rows, err := p.db.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

The following QueryContext is used to retrieve data and don’t forget that because this return has multiple rows, we also need to close the connection for each row at the end of the function. Here we use defer so that it is executed after returning to the main function.

defer rows.Close()

Next we will take data from rows to move it into a struct array.

for rows.Next() {
  var album Album
  err := rows.Scan(
    &album.ID,
    &album.Title,
    &album.Artist,
    &album.Price,
  )
  if err != nil {
    return nil, err
  }

  albums = append(albums, album)
}

And ends with the command return albums, nil in the function.

Create Batch Add Album

In this function we will create data batching where we send data in more than one quantity. We can use this for data that we send at once. Let’s take a look at the function we are going to create.

func (p *MSSQLService) BatchCreate(albums []Album) error {
	tx, err := p.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	query := `INSERT INTO dbo.album (title, artist, price) VALUES (@title, @artist, @price)`

	for _, album := range albums {
		_, err := tx.ExecContext(ctx, query,
			sql.Named("title", album.Title),
			sql.Named("artist", album.Artist),
			sql.Named("price", album.Price))
		if err != nil {
			log.Printf("error execute insert err: %v", err)
			continue
		}
	}

	err = tx.Commit()
	if err != nil {
		return err
	}

	return nil
}

In this batch function we use several transaction batching commands into the database. In the initial stage we will open a transaction to the DB with the command below.

tx, err := p.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

The Begin() function is used to open a transaction and ends with defer tx.Rollback() if something happens in the middle that results in an error.

Next, we repeat to insert data into the database.

for _, album := range albums {
  _, err := tx.ExecContext(ctx, query,
    sql.Named("title", album.Title),
    sql.Named("artist", album.Artist),
    sql.Named("price", album.Price))
  if err != nil {
    log.Printf("error execute insert err: %v", err)
    continue
  }
}

When creating data into the database, we use the ExecContext function which is useful for storing it temporarily in the database. And ends with Commit() to save permanently to the database.

err = tx.Commit()
if err != nil {
  return err
}

Change album data

To make changes to one data we need to create a function that sends the id parameter to select the data to be changed. Below are the complete functions for changing albums.

func (p *MSSQLService) Update(album Album) error {
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	query := `UPDATE dbo.album set title=@title, artist=@artist, price=@price WHERE id=@id`
	result, err := p.db.ExecContext(ctx, query,
		sql.Named("title", album.Title),
		sql.Named("artist", album.Artist),
		sql.Named("price", album.Price),
		sql.Named("id", album.ID))
	if err != nil {
		return err
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return err
	}

	fmt.Printf("Affected update : %d", rows)
	return nil
}

The ExecContext command is used the same as add because we can use this operation as long as the query needs to be updated, in this case we use it to update data.

query := `UPDATE dbo.album set title=@title, artist=@artist, price=@price WHERE id=@id`
result, err := p.db.ExecContext(ctx, query,
  sql.Named("title", album.Title),
  sql.Named("artist", album.Artist),
  sql.Named("price", album.Price),
  sql.Named("id", album.ID))
if err != nil {
  return err
}

The next command is to confirm whether the data has been successfully changed or not. Here we use the RowsAffected() function which ensures whether there are rows of data that have been updated after this operation or not.

rows, err := result.RowsAffected()
if err != nil {
  return err
}

Delete Album data

The operation or function to delete data in this program we delete one by one based on the input parameters of the function. Here are the complete functions.

func (p *MSSQLService) Delete(id int64) error {
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	query := `DELETE from dbo.album WHERE id=@id`
	result, err := p.db.ExecContext(ctx, query, sql.Named("id", id))
	if err != nil {
		return err
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return err
	}
	fmt.Printf("Affected delete : %d", rows)
	return nil
}

In this function we call ExecContext to delete data into the database. This is used the same as update and insert.

query := `DELETE from dbo.album WHERE id=@id`
	result, err := p.db.ExecContext(ctx, query, sql.Named("id", id))
	if err != nil {
		return err
	}

Followed by the RowsAffected() command to ensure that the data we want to delete is successful or not found in the database.

rows, err := result.RowsAffected()
if err != nil {
  return err
}

Initialize the Service module on Main

After we create a service program in which we create each function for operations in the database, it’s time for us to integrate all of this into the main program. Below is the complete function play.


type app struct {
	AlbumService services.AlbumService
}

func main() {
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatalf("Error loading .env file")
	}

	db, err := openDB(os.Getenv("MSSQL_URL"), true)
	if err != nil {
		log.Fatalln(err)
	}
	defer func(db *sql.DB) {
		err := db.Close()
		if err != nil {
			log.Fatalln(err)
		}
	}(db)

	application := app{AlbumService: services.NewMSSQLService(db)}

	err = application.AlbumService.BatchCreate([]services.Album{
		{Title: "Hari Yang Cerah", Artist: "Peterpan", Price: 50000},
		{Title: "Sebuah Nama Sebuah Cerita", Artist: "Peterpan", Price: 50000},
		{Title: "Bintang Di surga", Artist: "Peterpan", Price: 60000},
	})
	if err != nil {
		log.Fatal(err)
	}

	albums, err := application.AlbumService.GetAllAlbum()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("all album : %v\n", albums)

	albumNo1, err := application.AlbumService.Get(albums[0].ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("album number 1 : %v\n", albumNo1)

	err = application.AlbumService.Create(&services.Album{
		Title: "Mungkin Nanti", Artist: "Peterpan", Price: 70000,
	})
	if err != nil {
		log.Fatal(err)
	}
	log.Println("Success Create Album!")

	albumNo1.Price = 70000
	err = application.AlbumService.Update(*albumNo1)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Success update album %v\n", albumNo1)

	err = application.AlbumService.Delete(albumNo1.ID)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Success delete id: %d!\n", albumNo1.ID)

	for _, alb := range albums {
		err = application.AlbumService.Delete(alb.ID)
		log.Printf("error : %v", err)
	}
	log.Println("Success delete all data from table album")
}

func openDB(connString string, setLimits bool) (*sql.DB, error) {
	var err error
	db, err := sql.Open("sqlserver", connString)
	if err != nil {
		log.Fatal("Error creating connection pool: ", err.Error())
	}

	if setLimits {
		fmt.Println("setting limits")
		db.SetMaxIdleConns(5)
		db.SetMaxOpenConns(5)
	}

	ctx := context.Background()
	err = db.PingContext(ctx)
	if err != nil {
		log.Fatal(err.Error())
	}
	fmt.Printf("Connected!\n")
	return db, nil
}

Let’s discuss one by one what commands are executed in the main function.

Retrieves configuration from .env file

In this main program, we save all connection configurations in the database, stored in separate files, for example we save them in the .env file.

err := godotenv.Load(".env")
if err != nil {
  log.Fatalf("Error loading .env file")
}

The command above is used to load configuration data in the .env file. then saved to the env of the server or computer it is running on. Usually you can retrieve data from the environment with a command like this.

os.Getenv("<name-configuration>")

In this case we save the URL connection in a database that is stored in the environment so we need to retrieve the configuration data using the command os.Getenv("MSSQL_URL").

Next, we initialize the connection to the database.

db, err := openDB(os.Getenv("MSSQL_URL"), true)
if err != nil {
  log.Fatalln(err)
}

So now we also need to make sure this database connection needs to end with close. We can also use defer.

defer func(db *sql.DB) {
  err := db.Close()
  if err != nil {
    log.Fatalln(err)
  }
}(db)

In order for our service to access the main program, we need to initialize the service module.

application := app{AlbumService: services.NewMSSQLService(db)}

The rest is calling the service functions that we have defined in the function.

comments powered by Disqus