Apa itu MQTT ?

MQTT (Message Queuing Telemetry Transport) merupakah protokol yang digunakan untuk IoT (internet of things). Protokol ini biasanya digunakan untuk komunikasi antar mesin ke mesin, misal nya seperti perangkat arduino, raspi dan lain - lain. Protokol MQTT ini dirancang khusus untuk komunikasi mesin ke mesin dan protokol ini juga berjalan diatas TCP/IP. Berbeda dengan protokol HTTP, protokol MQTT menggunakan mekanisme publish subscribe, dimana penggunaan nya sama seperti message queue seperti Active MQ, Apache Kafka, Rabbit MQ dan lain - lain.

Apa itu EMQX ?

EMQX adalah salah satu broker MQTT yang cukup populer bersifat opensource dan termasuk broker yang banyak menawarkan fitur untuk kebutuhan IoT. EMQX juga menyediakan versi enterprise jika kita membutuhkan fitur - fitur tambahan, misal Enterprise Data Integration (fitur integrasi dengan external database seperti oracle, postgresql dan lain - lain).

Instalasi EMQX

Pada artikel ini, penulis akan menggunakan 2 buat vm yaitu :

  1. VM Node 1 : berfungsi sebagai node master, node master berfungsi untuk primary node nya.
  2. VM Node 2 : berfungsi sebagai node worker, dimana node worker akan melakukan join cluster ke node master.

Requirment Per Node

Adapun kebutuhan untuk per node adalah :

  • Ram 1 GB
  • Disk 10 GB
  • IP 192.168.50.2 (node 1)
  • IP 192.168.50.3 (node 2)
  • Centos 7

Untuk memudahkan setup VM per masing - masing node, penulis mencoba menggunakan vagrant. Berikut adalah konfigurasi Vagrantfile yang digunakan oleh penulis.

Vagrant.configure("2") do |config|

  config.vm.provider "virtualbox" do |v|
    v.memory = 1024
  end

  config.vm.define "node1" do |node|
    node.vm.box = "centos/7"
    node.vm.hostname = 'node1'
    node.vm.network "private_network", ip: "192.168.56.2"
  end

  config.vm.define "node2" do |node|
    node.vm.box = "centos/7"
    node.vm.hostname = 'node2'
    node.vm.network "private_network", ip: "192.168.56.3"
  end
end

Berikut adalah gambaran arsitektur yang penulis gunakan untuk membuat cluster EMQX.

mqtt-arsitektur.png

Lalu jalankan vagrant dengan perintah

vagrant up

Selanjutnya silahkan akses node 1 dengan perintah

vagrant ssh node1

Setalah masuk ke node 1, silahkan jalankan perintah berikut untuk melakukan instalasi

sudo yum update -y && sudo yum install wget -y
sudo mkdir /apps
sudo chown -R vagrant:vagrant /apps/
cd /apps
wget https://www.emqx.com/en/downloads/broker/5.0.8/emqx-5.0.8-el7-amd64.tar.gz
mkdir -p emqx && tar -zxvf emqx-5.0.8-el7-amd64.tar.gz -C emqx

Dan lakukan juga perintah tersebut pada node 2.

Setup EMQX

Setelah melakukan instalasi EMQX, tahap selanjutnya adalah melakukan setup cluster untuk EMQX. Setup yang dilakukan ada 3 bagian yaitu tunning server, setup node 1 dan setup node 2.

Tunning Server dan TCP Network

Ada beberapa yang harus dilakukan tunning baik disisi server maupun disisi TCP Network. Berikut adalah tahapan nya

Tunning kernel linux

tunning ini wajib dilakukan dengan menggunakan user root. Silahkan jalankan perintah berikut

sysctl -w fs.file-max=2097152
sysctl -w fs.nr_open=2097152
echo 2097152 > /proc/sys/fs/nr_open
ulimit -n 1048576

Silahkan buka file /etc/sysctl.conf lalu tambahkan code berikut

fs.file-max = 1048576

Kemudian buka file /etc/systemd/system.conf dan tambahkan juga code berikut

DefaultLimitNOFILE=1048576

dan yang terakhir silahkan buka file /etc/security/limits.conf dan tambahkan code berikut

*      soft    nofile      1048576
*      hard    nofile      1048576

Tunning TCP Network

Untuk melakukan tunning disisi TCP Network, cukup jalankan perintah berikut

sysctl -w net.core.somaxconn=32768
sysctl -w net.ipv4.tcp_max_syn_backlog=16384
sysctl -w net.core.netdev_max_backlog=16384
sysctl -w net.ipv4.ip_local_port_range='1000 65535'
sysctl -w net.core.rmem_default=262144
sysctl -w net.core.wmem_default=262144
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.core.optmem_max=16777216
sysctl -w net.ipv4.tcp_rmem='1024 4096 16777216'
sysctl -w net.ipv4.tcp_wmem='1024 4096 16777216'
sysctl -w net.nf_conntrack_max=1000000
sysctl -w net.netfilter.nf_conntrack_max=1000000
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sysctl -w net.ipv4.tcp_max_tw_buckets=1048576
sysctl -w net.ipv4.tcp_fin_timeout=15

Tuning Erlang VM dan EMQX

Untuk tuning erlang VM, silahkan buka file emqx.conf di folder /apps/emqx/etc, lalu ubah pada bagian node menjadi

node {
  name = "emqx@192.168.56.3"
  cookie = "emqxcookie"
  data_dir = "data"
  process_limit = 2097152
  max_ports = 2097152
}

Sedangkan untuk EMQX, masih di file yang sama, silahkan ubah pada bagian

listeners.tcp.default {
  bind = "0.0.0.0:1883"
  acceptors = 64
  max_connections = 1024000
}

listeners.ssl.default {
  bind = "0.0.0.0:8883"
  acceptors = 64
  max_connections = 1024000
  ssl_options {
    keyfile = "etc/certs/key.pem"
    certfile = "etc/certs/cert.pem"
    cacertfile = "etc/certs/cacert.pem"
  }
}

listeners.ws.default {
  bind = "0.0.0.0:8083"
  acceptors = 64
  max_connections = 1024000
  websocket.mqtt_path = "/mqtt"
}

listeners.wss.default {
  bind = "0.0.0.0:8084"
  acceptors = 64
  max_connections = 1024000
  websocket.mqtt_path = "/mqtt"
  ssl_options {
    keyfile = "etc/certs/key.pem"
    certfile = "etc/certs/cert.pem"
    cacertfile = "etc/certs/cacert.pem"
  }
}

Setup Node 1

Untuk node 1, silahkan buka file emqx.conf di folder /apps/emqx/etc. Cari node.name, lalu ubah emqx@127.0.0.1 menjadi emqx@<<IP>> dan juga node.cookie seperti berikut.

node {
  name = "emqx@192.168.56.2"
  cookie = "emqxcookie"
  data_dir = "data"
}

Setelah selesai, silahkan jalankan emqx dengan perintah

/apps/emqx/bin/emqx start

jika sukses maka akan muncul output seperti berikut.

EMQX 5.0.8 is started successfully!

Setup Node 2

Untuk node 2, sebenarnya sama seperti node 1 yaitu silahkan buka file emqx.conf di folder /apps/emqx/etc. Cari node.name, lalu ubah emqx@127.0.0.1 menjadi emqx@<<IP>> dan juga node.cookie seperti berikut.

node {
  name = "emqx@192.168.56.3"
  cookie = "emqxcookie"
  data_dir = "data"
}

Setelah selesai, silahkan jalankan emqx dengan perintah

/apps/emqx/bin/emqx start

jika sukses maka akan muncul output seperti berikut.

EMQX 5.0.8 is started successfully!

lalu pada node 2 agar dapat join ke cluster, silahkan jalankan perintah berikut.

/apps/emqx/bin/emqx ctl cluster join emqx@192.168.56.2

jika berhasil maka akan muncul output seperti berikut.

Join the cluster successfully.
Cluster status: #{running_nodes => ['emqx@192.168.56.2','emqx@192.168.56.3'],
                  stopped_nodes => []}

atau jika kita ingik melakukan pengecekan statuc cluster dapat menggunakan perintah

/apps/emqx/bin/emqx ctl cluster status

Untuk mengetahui apakah emqx nya sudah berjalan atau tidak, silahkan buka http://192.168.56.2:18083/ di browser. 18083 merupakan port dari dashboard emqx.

Screenshot from 2023-11-11 14-59-53.png

Lalu silahkan login dengan menggunakan user admin password public. Setelah berhasil login, kita dapat melihat jumlah dari node emqx beserta cluster nya.

Screenshot from 2023-11-11 15-00-14.png

Screenshot from 2023-11-11 15-00-27.png

Sekian artikel mengenai instalasi dan setup EMQX. Jika ada saran dan komentar silahkan isi dibawah dan terima kasih :).

Apa Itu Projections ?

Projections Pada Spring Data JPA adalah salah satu fitur yang ditawarkan oleh spring yang memudahkan developer untuk membuat permodelan dengan tipe tertentu atau biasa nya lebih disebut dengan permodelan dengan custom DTO.

Biasa nya projections akan digunakan ketika developer membutuhkan field - field yang tidak tersedia pada class entity, misal nya seperti hasil dari sum(), avg() dan lain sebagai nya. Projections juga bisa digunakan untuk custom query seperti sub query, dimana hasil dari query tersebut tidak dapat di mapping ke class entity. Projections pada spring data jpa dapat di implementasikan dengan 3 metode yaitu interface projections, class projections (DTO) dan dynamic projections.

Contoh penggunaan projections, misal kita mempunyai sebuah class entity seperti berikut.

public class Product {

    @Id
    private UUID id;

    private String name;

    private BigDecimal price;

    private Integer quantity;
}

Dari class entity diatas, misal kita ingin menampilkan nama dan harga product saja. Sebenarnya kita bisa menggunakan class DTO secara manual cuma dibutuhkan proses mapping terlebih dahulu, sehingga dibutuhkan mapping dari class entity ke class DTO. Tapi di case tertentu seperti process sum terhadap suatu column misal jika kita menggunakan entity diatas, maka kita ingin mengetahui berapa total price. Hasil dari sum tersebut tidak mempunyai attribute pada class entity diatas sehingga dibutuhkan proses projections.

Setup Kebutuhan Belajar Projections

Sebelum memulai case per case projections, kita perlu melakukan setup project untuk kebutuhan belajar projections. Silahkan akses web spring initializer, lalu silahkan setup seperti berikut.

Screenshot from 2023-10-18 23-10-07.png

Lalu silahkan buka project tersebut dengan menggunakan editor kesayangan anda, lalu silahkan buat 1 class entity Product lalu masukkan codingan seperti berikut.

package org.rizki.mufrizal.belajar.projections.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.UuidGenerator;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.UUID;

@Entity
@Table(name = "tb_product")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Product implements Serializable {

    @Id
    @UuidGenerator(style = UuidGenerator.Style.TIME)
    @GeneratedValue
    private UUID id;

    private String name;

    private BigDecimal price;

    private Integer quantity;
}

Lalu silahkan buat 1 class repository yaitu ProductRepository dan masukkan codingan berikut

package org.rizki.mufrizal.belajar.projections.repository;

import org.rizki.mufrizal.belajar.projections.domain.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.UUID;

public interface ProductRepository extends JpaRepository<Product, UUID> {
}

Untuk inisialisasi data nya, kita dapat menggunakan annotation EventListener, silahkan buka class BelajarProjectionsApplication lalu tambahkan code berikut

package org.rizki.mufrizal.belajar.projections;

import org.rizki.mufrizal.belajar.projections.domain.Product;
import org.rizki.mufrizal.belajar.projections.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;

import java.math.BigDecimal;

@SpringBootApplication
public class BelajarProjectionsApplication {

    @Autowired
    private ProductRepository productRepository;

    public static void main(String[] args) {
        SpringApplication.run(BelajarProjectionsApplication.class, args);
    }

    @EventListener
    public void onApplicationEvent(ApplicationReadyEvent event) {
        for (int i = 1; i <= 5; i++) {
            var product = new Product();
            product.setName("Air" + i);
            product.setPrice(BigDecimal.valueOf(1000));
            product.setQuantity(10);

            productRepository.save(product);
        }
    }
}

Kemudian untuk config koneksi ke database, silahkan buka file application.properties lalu ubah menjadi seperti berikut

spring.datasource.url=jdbc:h2:mem:belajar_projectios
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=create-drop

Interface Projections

Interface projections merupakan salah satu metode projections yang sangat mudah di implementasikan. Kita cukup membuat sebuah class interface, dimana di dalam class tersebut terdapat method dari masing - masing property yang hendak kita akses.

Pada case pertama, kita akan menggunakan projection untuk mengambil field - field tertentu, misal pada tulisan ini penulis hanya ingin melakukan select untuk nama dan price product saja. Maka yang diperlukan adalah silahkan buat 1 class interface dengan nama ProductHQL lalu masukkan codingan berikut.

package org.rizki.mufrizal.belajar.projections.domain.hql;

import java.math.BigDecimal;

public interface ProductHQL {
    String getName();

    BigDecimal getPrice();
}

Bisa dilihat dari codingan diatas, untuk dapat melakukan mapping dari hasil query HQL maka diperlukan membuat method getter, dimana kita cukup membuat method untuk field - field yang diperlukan saja. Cara implementasi sangat mudah yaitu cukup menambahka code berikut pada class ProductRepository

package org.rizki.mufrizal.belajar.projections.repository;

import org.rizki.mufrizal.belajar.projections.domain.Product;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductHQL;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.UUID;

public interface ProductRepository extends JpaRepository<Product, UUID> {
    List<ProductHQL> findAllBy();
}

Disini dapat dilihat, kita akan mencoba melakukan query ke database tapi hasil nya ingin ditampung ke dalam class interface dimana class interface tersebut hanya berisi 2 field saja. Fungsi projection disini adalah melakukan mapping dari hasil HQL tersebut ke class interface yang memiliki 2 method.

Lalu untuk dapat melakukan test, Silahkan buat 1 class controller yaitu ProductController lalu isi code nya seperti berikut.

package org.rizki.mufrizal.belajar.projections.controller;

import org.rizki.mufrizal.belajar.projections.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @Autowired
    private ProductRepository productRepository;

    @GetMapping(value = "/api/product/interface")
    public ResponseEntity<?> productInterface() {
        return new ResponseEntity<>(productRepository.findAllBy(), HttpStatus.OK);
    }

}

Setelah selesai, silahkan jalankan command mvn clean spring-boot:run untuk menjalankan spring boot nya lalu kamu bisa akses ke browser dengan url http://localhost:8080/api/product/interface atau dapat menggunakan curl dengan command

curl http://localhost:8080/api/product/interface -v

Hasil nya berupa

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/product/interface HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Thu, 19 Oct 2023 04:25:02 GMT
< 
* Connection #0 to host localhost left intact
[{"name":"Air1","price":1000.00},{"name":"Air2","price":1000.00},{"name":"Air3","price":1000.00},{"name":"Air4","price":1000.00},{"name":"Air5","price":1000.00}]

Kemudian klau disisi log spring boot nya, hibernate hanya melakukan select untuk 2 field saja, berikut hasil log sql nya.

select
    p1_0.name,
    p1_0.price
from
    tb_product p1_0

Pertanyaan selanjutnya adalah, apakah projections dengan interface ini dapat dipadukan dengan custom HQL/SQL ? jawaban nya adalah bisa. Silahkan tambahkan code berikut pada class ProductRepository

package org.rizki.mufrizal.belajar.projections.repository;

import org.rizki.mufrizal.belajar.projections.domain.Product;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductHQL;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.UUID;

public interface ProductRepository extends JpaRepository<Product, UUID> {
    List<ProductHQL> findAllBy();

    @Query("select p.name as name, p.price as price from Product p")
    List<ProductHQL> findAllCustomQuery();
}

Kemudian tambahkan code berikut untuk ProductController agar kita dapat melakukan test

package org.rizki.mufrizal.belajar.projections.controller;

import org.rizki.mufrizal.belajar.projections.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @Autowired
    private ProductRepository productRepository;

    @GetMapping(value = "/api/product/interface")
    public ResponseEntity<?> productInterface() {
        return new ResponseEntity<>(productRepository.findAllBy(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/interface/query")
    public ResponseEntity<?> productInterfaceQuery() {
        return new ResponseEntity<>(productRepository.findAllCustomQuery(), HttpStatus.OK);
    }

}

Kemudian silahkan akses url ke http://localhost:8080/api/product/interface/query dengan browser atau curl maka hasil nya akan sama seperti sebelum nya.

Class-based Projections (DTOs)

Projection selanjutnya yaitu menggunakan class based atau biasa nya sering disebut dengan DTO (Data Transfer Objects). Berbeda dengan interface, class based projections biasa nya akan dilakukan mapping langsung dari hasil query ke class DTO. Pada artikel ini, penulis akan memberikan contoh penggunaan class based projections yaitu dengan melakukan proses sum terhadap column price. Silahkan buat sebuah class dengan nama ProductPriceHQL di dalam package org.rizki.mufrizal.belajar.projections.domain.hql

package org.rizki.mufrizal.belajar.projections.domain.hql;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;
import java.math.BigDecimal;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ProductPriceHQL implements Serializable {
    private BigDecimal price;
}

Kemudian pada class ProductRepository ubah codingan nya seperti berikut

package org.rizki.mufrizal.belajar.projections.repository;

import org.rizki.mufrizal.belajar.projections.domain.Product;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductHQL;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductPriceHQL;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.UUID;

public interface ProductRepository extends JpaRepository<Product, UUID> {
    List<ProductHQL> findAllBy();

    @Query("select p.name as name, p.price as price from Product p")
    List<ProductHQL> findAllCustomQuery();

    @Query("select new org.rizki.mufrizal.belajar.projections.domain.hql.ProductPriceHQL(sum(p.price)) from Product p")
    ProductPriceHQL getAllPrice();
}

Dapat dilihat pada method getAllPrice(), kita menggunakan query HQL/JPQL dan dapat langsung melakukan mapping dari hasil sum price, akan tetapi proses query seperti ini memiliki kekurangan jika terdapat sub query pada parent query nya dan hanya bisa menggunakan HQL/JPQL sehingga tidak memungkinkan jika kita menggunakan native query. Selanjutnya silahkan ubah codingan pada class ProductController seperti berikut.

package org.rizki.mufrizal.belajar.projections.controller;

import org.rizki.mufrizal.belajar.projections.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @Autowired
    private ProductRepository productRepository;

    @GetMapping(value = "/api/product/interface")
    public ResponseEntity<?> productInterface() {
        return new ResponseEntity<>(productRepository.findAllBy(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/interface/query")
    public ResponseEntity<?> productInterfaceQuery() {
        return new ResponseEntity<>(productRepository.findAllCustomQuery(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/dto/query")
    public ResponseEntity<?> productDtoQuery() {
        return new ResponseEntity<>(productRepository.getAllPrice(), HttpStatus.OK);
    }

}

Lalu jalankan kembali spring boot nya dan silahkan jalankan command berikut untuk mengakses api tersebut.

curl http://localhost:8080/api/product/dto/query -v

maka hasil nya

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/product/dto/query HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 04 Nov 2023 03:56:09 GMT
< 
* Connection #0 to host localhost left intact
{"price":5000.00}

Kemudian jika di cek disisi query nya maka hibernate akan membuat query seperti berikut

select
    sum(p1_0.price) 
from
    tb_product p1_0

Pertanyaan selanjutnya adalah, apakah memungkinkan class based projections menggunakan native query ? jawaban nya adalah bisa dengan menggunakan bantuan annotation @NamedNativeQuery untuk deklarasi query native dan annotation @SqlResultSetMapping untuk proses mapping dari hasil native query ke class DTO. Penggunaan annotation @NamedNativeQuery dan @SqlResultSetMapping diharuskan di class entity atau domain yang telah kita define. Yang pertama kita lakukan adalah membuat query native nya, misalkan kita menggunakan query sederhana untuk mengambil semua data

select id as id, name as nama, price as harga, quantity as jumlah
from tb_product;

Setelah mengetahui hasil query nya, silahkan buat sebuah class ProductCustomHQL di dalam package org.rizki.mufrizal.belajar.projections.domain.hql dan masukkan codingan berikut

package org.rizki.mufrizal.belajar.projections.domain.hql;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;
import java.math.BigDecimal;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ProductCustomHQL implements Serializable {
    private String id;

    private String nama;

    private BigDecimal harga;

    private Integer jumlah;
}

Lalu silahkan buka class Product di dalam package org.rizki.mufrizal.belajar.projections.domain lalu ubah codingan nya seperti berikut

package org.rizki.mufrizal.belajar.projections.domain;

import jakarta.persistence.ColumnResult;
import jakarta.persistence.ConstructorResult;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.NamedNativeQuery;
import jakarta.persistence.SqlResultSetMapping;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.UuidGenerator;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductCustomHQL;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.UUID;

@Entity
@Table(name = "tb_product")
@NamedNativeQuery(
        name = "findAllProduct",
        query = "select id as id, name as nama, price as harga, quantity as jumlah " +
                "from tb_product",
        resultSetMapping = "ProductCustomHQL"
)
@SqlResultSetMapping(
        name = "ProductCustomHQL",
        classes = @ConstructorResult(
                targetClass = ProductCustomHQL.class,
                columns = {
                        @ColumnResult(name = "id", type = String.class),
                        @ColumnResult(name = "nama", type = String.class),
                        @ColumnResult(name = "harga", type = BigDecimal.class),
                        @ColumnResult(name = "jumlah", type = Integer.class)
                }
        )
)
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Product implements Serializable {

    @Id
    @UuidGenerator(style = UuidGenerator.Style.TIME)
    @GeneratedValue
    private UUID id;

    private String name;

    private BigDecimal price;

    private Integer quantity;
}

Dari codingan diatas, dapat dilihat pada annotation @SqlResultSetMapping kita mendefinisikan nama dari result nya yang nanti dapat di panggil di annotation @NamedNativeQuery. Kemudian kita juga dapat melihat ada proses mapping dengan target class dan masing - masing nama column nya. Sedangkan pada annotation @NamedNativeQuery dapat kita lihat terdapat nama dari native query kemudian kita juga mendeklarasikan query dan result set mapping nya. Langkan selanjutnya, kita hanya perlu melakukan pemanggilan nama dari native query pada class repository, silahkan buka class ProductRepository lalu ubah codingan nya seperti berikut.

package org.rizki.mufrizal.belajar.projections.repository;

import org.rizki.mufrizal.belajar.projections.domain.Product;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductCustomHQL;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductHQL;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductPriceHQL;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.UUID;

public interface ProductRepository extends JpaRepository<Product, UUID> {
    List<ProductHQL> findAllBy();

    @Query("select p.name as name, p.price as price from Product p")
    List<ProductHQL> findAllCustomQuery();

    @Query("select new org.rizki.mufrizal.belajar.projections.domain.hql.ProductPriceHQL(sum(p.price)) from Product p")
    ProductPriceHQL getAllPrice();

    @Query(name = "findAllProduct", nativeQuery = true)
    List<ProductCustomHQL> findAllProduct();
}

Dari findAllProduct kita dapat melihat bahwa kita cukup memanggil nama native query nya saja lalu hasil nya akan disimpan ke dalam class ProductCustomHQL. Ini merupakan metode yang bisa digunakan pada projection class based dengan native query. Lalu buka class ProductController dan ubah codingan nya seperti berikut.

package org.rizki.mufrizal.belajar.projections.controller;

import org.rizki.mufrizal.belajar.projections.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @Autowired
    private ProductRepository productRepository;

    @GetMapping(value = "/api/product/interface")
    public ResponseEntity<?> productInterface() {
        return new ResponseEntity<>(productRepository.findAllBy(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/interface/query")
    public ResponseEntity<?> productInterfaceQuery() {
        return new ResponseEntity<>(productRepository.findAllCustomQuery(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/dto/query")
    public ResponseEntity<?> productDtoQuery() {
        return new ResponseEntity<>(productRepository.getAllPrice(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/dto/custom")
    public ResponseEntity<?> productDtoCustom() {
        return new ResponseEntity<>(productRepository.findAllProduct(), HttpStatus.OK);
    }

}

Lalu jalankan kembali spring boot nya dan jalankan perintah berikut untuk melakukan test api nya

curl http://localhost:8080/api/product/dto/query -v

dan hasil nya

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/product/dto/custom HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 04 Nov 2023 04:16:48 GMT
< 
* Connection #0 to host localhost left intact
[{"id":"7f000101-8b98-1c1c-818b-988c204e0000","nama":"Air1","harga":1000.00,"jumlah":10},{"id":"7f000101-8b98-1c1c-818b-988c206e0001","nama":"Air2","harga":1000.00,"jumlah":10},{"id":"7f000101-8b98-1c1c-818b-988c20700002","nama":"Air3","harga":1000.00,"jumlah":10},{"id":"7f000101-8b98-1c1c-818b-988c20710003","nama":"Air4","harga":1000.00,"jumlah":10},{"id":"7f000101-8b98-1c1c-818b-988c20710004","nama":"Air5","harga":1000.00,"jumlah":10}]

kalau dilihat dari query yang dijalankan

select
    id as id,
    name as nama,
    price as harga,
    quantity as jumlah 
from
    tb_product

dapat dilihat hibernate menjalankan query native tersebut.

Dynamic Projections

Dynamic projections biasa nya digunakan untuk melakukan mapping ke banyak class tapi menggunakan query yang sama. Dynamic projections akan memanfaatkan java generic untuk dapat melakukan dynamic mapping. Misal contoh kasus kita hanya ingin mengeluarkan nama dan harga barang saja. Silahkan buat 1 class dengan nama ProductNamePriceHQL di package org.rizki.mufrizal.belajar.projections.domain.hql

package org.rizki.mufrizal.belajar.projections.domain.hql;

import java.math.BigDecimal;

public interface ProductNamePriceHQL {
    String getName();

    BigDecimal getPrice();
}

Lalu pada class ProductRepository silahkan ubah menjadi seperti berikut

package org.rizki.mufrizal.belajar.projections.repository;

import org.rizki.mufrizal.belajar.projections.domain.Product;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductCustomHQL;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductHQL;
import org.rizki.mufrizal.belajar.projections.domain.hql.ProductPriceHQL;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.Collection;
import java.util.List;
import java.util.UUID;

public interface ProductRepository extends JpaRepository<Product, UUID> {
    List<ProductHQL> findAllBy();

    @Query("select p.name as name, p.price as price from Product p")
    List<ProductHQL> findAllCustomQuery();

    @Query("select new org.rizki.mufrizal.belajar.projections.domain.hql.ProductPriceHQL(sum(p.price)) from Product p")
    ProductPriceHQL getAllPrice();

    @Query(name = "findAllProduct", nativeQuery = true)
    List<ProductCustomHQL> findAllProduct();

    <T> T findByName(String name, Class<T> type);
}

bisa dilihat di baris akhir kita menggunakan java generic sehingga kita dapat menggunakan class apa pun untuk melakukan mapping result nya. Lalu silahkan buka class ProductController dan ubah seperti berikut.

package org.rizki.mufrizal.belajar.projections.controller;

import org.rizki.mufrizal.belajar.projections.domain.hql.ProductNamePriceHQL;
import org.rizki.mufrizal.belajar.projections.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @Autowired
    private ProductRepository productRepository;

    @GetMapping(value = "/api/product/interface")
    public ResponseEntity<?> productInterface() {
        return new ResponseEntity<>(productRepository.findAllBy(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/interface/query")
    public ResponseEntity<?> productInterfaceQuery() {
        return new ResponseEntity<>(productRepository.findAllCustomQuery(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/dto/query")
    public ResponseEntity<?> productDtoQuery() {
        return new ResponseEntity<>(productRepository.getAllPrice(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/dto/custom")
    public ResponseEntity<?> productDtoCustom() {
        return new ResponseEntity<>(productRepository.findAllProduct(), HttpStatus.OK);
    }

    @GetMapping(value = "/api/product/dynamic")
    public ResponseEntity<?> productDynamic(@RequestParam(name = "name") String name) {
        return new ResponseEntity<>(productRepository.findByName(name, ProductNamePriceHQL.class), HttpStatus.OK);
    }

}

Jika telah selesai lalu jalankan spring boot kembali dan jalankan perintah berikut

curl http://localhost:8080/api/product/dynamic\?name\=Air1 -v

dan hasil nya

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/product/dynamic?name=Air1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 04 Nov 2023 05:22:57 GMT
< 
* Connection #0 to host localhost left intact
{"name":"Air1","price":1000.00}

dari query HQL nya akan muncul seperti berikut

select
    p1_0.name,
    p1_0.price 
from
    tb_product p1_0 
where
    p1_0.name=?

Dari penjelasan diatas, dapat disimpulkan spring data JPA dapat menggunakan projections dengan 3 metode projections yaitu interace, class based (DTO) dan dynamic. Masing - masing projections digunakan pada kasus - kasus tertentu seperi custom query, sub query dan lain sebagain nya.

Sekian artikel mengenai Belajar projection pada spring data jpa, untuk source code diatas dapat anda akses di Belajar-Projections. Jika ada saran dan komentar silahkan isi dibawah dan terima kasih :).

Apa Itu Testcontainers ?

Testcontainers adalah framework yang menyediakan ketersediaan environment dan infrastruktur untuk kebutuhan automate test.

Latar belakang dibuat nya testcontainers adalah untuk mempermudah developer pada saat mejalankan unit test tanpa perlu melakukan setup infrastruktur seperti database, message broker dan lain sebagai nya. Testcontainers nanti nya akan menjalankan kebutuhan inftrastuktur aplikasi yang dibutuhkan dengan menggunakan container (biasa nya menggunakan docker). Dengan ada nya testcontainers, maka developer dapat fokus pada pembuatan unit test dan integration test.

Untuk saat ini, testcontainers hanya memiliki module - module tertentu saja sehingga masih terdapat keterbatasan untuk module - module yang tidak common. Contoh module yang dapat kita gunakan adalah seperti database postgresql, mysql, mongodb, kafka dan lain sebagai nya. Tetapi untuk saat ini, module - module yang terdapat pada testcontainers baru di support untuk bahasa pemrograman java, noed js, go dan .NET, berikut list module - module nya.

Setup Testcontainers Pada Spring Boot

Pada artikel ini, penulis akan mencoba mendemokan dengan menggunakan spring boot. Secara default, spring boot sudah support dengan testcontainers dan juga integration test sehingga dapat mempermudah developer dalam menggunakan module - module testcontainers. Seperti biasa, silahkan buka spring initializr. lalu isi seperti berikut

Screenshot from 2023-09-12 22-07-20.png

Lalu setelah selesai, silahkan download dan buka dengan IDE. Silahkan buka file pom.xml lalu tambahkan dependecy rest-assured seperti berikut

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.belajar.testcontainers</groupId>
    <artifactId>belajar-testcontainers</artifactId>
    <version>1.0.0</version>
    <name>belajar-testcontainers</name>
    <description>belajar testcontainers</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-testcontainers</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Silahkan buka file application.properties di dalam folder resources lalu ubah seperti berikut

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/belajar_testcontainers
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.show-sql=true
spring.jpa.properties.jakarta.persistence.sharedCache.mode=UNSPECIFIED
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=create-drop

Lalu pada folder test, silahkan buat sebuah class TestBelajarTestcontainersApplication dan juga file application.properties pada folder resources lalu ubah seperti berikut

spring.jpa.show-sql=true
spring.jpa.properties.jakarta.persistence.sharedCache.mode=UNSPECIFIED
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=create-drop

Membuat Domain Class

Domain class digunakan untuk keperluan mapping dari class ke table yang terdapat pada database. Silahkan buat sebuah package domain lalu silahkan buat class Product di dalam package tersebut dan ubah codingan nya seperti berikut.

package com.belajar.testcontainers.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.UuidGenerator;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.UUID;

@Entity
@Table(name = "tb_product")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product implements Serializable {
    @Id
    @UuidGenerator
    private UUID id;

    private String name;

    private BigDecimal price;

    private Integer quantity;
}

Membuat Repository Class

Repository class digunakan untuk process query ke database. Silahkan buat sebuah package repository lalu silahkan buat class ProductRepository di dalam package tersebut dan ubah codingan nya seperti berikut.

package com.belajar.testcontainers.repository;

import com.belajar.testcontainers.domain.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.UUID;

public interface ProductRepository extends JpaRepository<Product, UUID> {
}

Membuat Service Class

Service class digunakan untuk logic bisnis. Silahkan buat sebuah package service dan Service.impl lalu silahkan buat class ProductService di dalam package tersebut dan ubah codingan nya seperti berikut.

package com.belajar.testcontainers.service;

import com.belajar.testcontainers.domain.Product;

import java.util.Optional;
import java.util.UUID;

public interface ProductService {
    Product save(Product product);

    Optional<Product> findOne(UUID id);
}

Lalu buat sebuah class ProductServiceImpl di dalam package Service.impl dan ubah codingan nya seperti berikut.

package com.belajar.testcontainers.service.impl;

import com.belajar.testcontainers.domain.Product;
import com.belajar.testcontainers.repository.ProductRepository;
import com.belajar.testcontainers.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.UUID;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public Product save(Product product) {
        return productRepository.save(product);
    }

    @Override
    public Optional<Product> findOne(UUID id) {
        return productRepository.findById(id);
    }
}

Membuat Controller Class

Controller class digunakan untuk menerima request atau traffic dari client. Silahkan buat sebuah package controller lalu silahkan buat class ProductController di dalam package tersebut dan ubah codingan nya seperti berikut.

package com.belajar.testcontainers.controller;

import com.belajar.testcontainers.domain.Product;
import com.belajar.testcontainers.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping(value = "/api")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping(value = "/products/{id}")
    public ResponseEntity<?> findOne(@PathVariable("id") UUID id) {
        return new ResponseEntity<>(productService.findOne(id), HttpStatus.OK);
    }

    @PostMapping(value = "/products")
    public ResponseEntity<?> save(@RequestBody Product product) {
        return new ResponseEntity<>(productService.save(product), HttpStatus.OK);
    }

}

Membuat Testcontainers Class

Silahkan buka class TestBelajarTestcontainersApplication di dalam folder test, lalu tambahkan code berikut

package com.belajar.testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestConfiguration(proxyBeanMethods = false)
public class TestBelajarTestcontainersApplication {

}

fungsi dari annotation @SpringBootTest untuk menjalankan test pada spring boot, lalu spring boot akan dijalankan dengan menggunakan port random, sedangkan fungsi dari annotation @TestConfiguration digunakan untuk membuat unit test pada spring boot, melakukan override pada bean spring boot dan membuat bean baru sesuai dengan kebutuhan unit test. Selanjutnya untuk testcontainers pada artikel ini, kita akan menggunakan module postgresql, untuk dapat menjalankan module tersebut silahkan tambahkan code berikut.

package com.belajar.testcontainers;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestConfiguration(proxyBeanMethods = false)
public class TestBelajarTestcontainersApplication {

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:alpine"
    );

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @BeforeAll
    static void beforeAll() {
        postgres.start();
    }

    @AfterAll
    static void afterAll() {
        postgres.stop();
    }
}

Pada codingan diatas dapat dilihat bahwa penulis mendeklarasikan PostgreSQLContainer dimana image yang digunakan adalah postgres:alpine. Kemudian karena postgresql ini dijalankan pada testcontainer maka spring membutuhkan properties seperti url, username dan password sehingga class DynamicPropertyRegistry dapat mereplace properties tersebut. Pada annotation @BeforeAll dan @AfterAll berfungsi untuk menjalankan dan mematikan postgresql. Selanjutkan kita memerlukan setup untuk kebutuhan REST Assured dengan menambahkan code berikut

package com.belajar.testcontainers;

import io.restassured.RestAssured;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestConfiguration(proxyBeanMethods = false)
public class TestBelajarTestcontainersApplication {

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:alpine"
    );

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @BeforeAll
    static void beforeAll() {
        postgres.start();
    }

    @LocalServerPort
    private Integer port;

    @BeforeEach
    void setUp() {
        RestAssured.baseURI = "http://localhost:" + port;
    }

    @AfterAll
    static void afterAll() {
        postgres.stop();
    }
}

Pada bagian annotation @LocalServerPort berfungsi untuk mengambil port spring boot yang akan berjalan pada saat unit test berjalan. Annotation @BeforeEach berfungsi untuk melakukan setup pada RestAssured setelah spring boot running dengan port random. Langkah selanjutnya silahkan tambahkan code berikut untuk melakukan test untuk save dan findOne ke database.

package com.belajar.testcontainers;

import com.belajar.testcontainers.domain.Product;
import com.belajar.testcontainers.repository.ProductRepository;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

import java.math.BigDecimal;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestConfiguration(proxyBeanMethods = false)
public class TestBelajarTestcontainersApplication {

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:alpine"
    );

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @LocalServerPort
    private Integer port;

    @BeforeAll
    static void beforeAll() {
        postgres.start();
    }

    @BeforeEach
    void setUp() {
        RestAssured.baseURI = "http://localhost:" + port;
    }

    @AfterAll
    static void afterAll() {
        postgres.stop();
    }

    @Autowired
    private ProductRepository productRepository;

    @Test
    void save() {
        var product = Product.builder()
                .name("aqua")
                .price(BigDecimal.valueOf(1000L))
                .quantity(5)
                .build();

        RestAssured.given()
                .contentType(ContentType.JSON)
                .body(product)
                .post("/api/products")
                .then()
                .statusCode(200)
                .body("name", Matchers.is(product.getName()));
    }


    @Test
    void testFindOne() {
        var product = Product.builder()
                .name("aqua")
                .price(BigDecimal.valueOf(1000L))
                .quantity(5)
                .build();

        var result = productRepository.save(product);

        RestAssured.given()
                .contentType(ContentType.JSON)
                .when()
                .get("/api/products/" + result.getId())
                .then()
                .statusCode(200)
                .body("name", Matchers.is(result.getName()));
    }
}

Lalu selanjutnya silahkan jalankan test nya dengan menggunakan command

mvn clean test

maka hasil nya akan seperti berikut

[INFO] Scanning for projects...
[INFO] 
[INFO] ---------< com.belajar.testcontainers:belajar-testcontainers >----------
[INFO] Building belajar-testcontainers 1.0.0
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- clean:3.2.0:clean (default-clean) @ belajar-testcontainers ---
[INFO] Deleting /home/rizki/Documents/project/belajar-project/spring-project/belajar-testcontainers/target
[INFO] 
[INFO] --- resources:3.3.1:resources (default-resources) @ belajar-testcontainers ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO] 
[INFO] --- compiler:3.11.0:compile (default-compile) @ belajar-testcontainers ---
[INFO] Changes detected - recompiling the module! :source
[INFO] Compiling 6 source files with javac [debug release 17] to target/classes
[INFO] 
[INFO] --- resources:3.3.1:testResources (default-testResources) @ belajar-testcontainers ---
[INFO] Copying 1 resource from src/test/resources to target/test-classes
[INFO] 
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ belajar-testcontainers ---
[INFO] Changes detected - recompiling the module! :dependency
[INFO] Compiling 1 source file with javac [debug release 17] to target/test-classes
[INFO] 
[INFO] --- surefire:3.0.0:test (default-test) @ belajar-testcontainers ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.belajar.testcontainers.TestBelajarTestcontainersApplication
13:36:39.856 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.belajar.testcontainers.TestBelajarTestcontainersApplication]: TestBelajarTestcontainersApplication does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
13:36:39.931 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.belajar.testcontainers.BelajarTestcontainersApplication for test class com.belajar.testcontainers.TestBelajarTestcontainersApplication
13:36:40.050 [main] INFO org.testcontainers.utility.ImageNameSubstitutor -- Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
13:36:40.188 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy -- Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
13:36:40.358 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy -- Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
13:36:40.359 [main] INFO org.testcontainers.DockerClientFactory -- Docker host IP address is localhost
13:36:40.372 [main] INFO org.testcontainers.DockerClientFactory -- Connected to docker: 
  Server Version: 24.0.6
  API Version: 1.43
  Operating System: Ubuntu 22.04.3 LTS
  Total Memory: 14812 MB
13:36:40.412 [main] INFO tc.testcontainers/ryuk:0.5.1 -- Creating container for image: testcontainers/ryuk:0.5.1
13:36:40.513 [main] INFO tc.testcontainers/ryuk:0.5.1 -- Container testcontainers/ryuk:0.5.1 is starting: 5bd8ac42d26ceb6ccbf70fef204729d955ad56c08787726138805e80d5c10639
13:36:40.848 [main] INFO tc.testcontainers/ryuk:0.5.1 -- Container testcontainers/ryuk:0.5.1 started in PT0.470571487S
13:36:40.852 [main] INFO org.testcontainers.utility.RyukResourceReaper -- Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
13:36:40.852 [main] INFO org.testcontainers.DockerClientFactory -- Checking the system...
13:36:40.853 [main] INFO org.testcontainers.DockerClientFactory -- ✔︎ Docker server version should be at least 1.6.0
13:36:40.856 [main] INFO tc.postgres:alpine -- Pulling docker image: postgres:alpine. Please be patient; this may take some time but only needs to be done once.
13:36:43.815 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Starting to pull image
13:36:43.826 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  0 pending,  0 downloaded,  0 extracted, (0 bytes/0 bytes)
13:36:44.968 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  7 pending,  1 downloaded,  0 extracted, (34 KB/? MB)
13:36:45.018 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  6 pending,  2 downloaded,  0 extracted, (34 KB/? MB)
13:36:46.230 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  5 pending,  3 downloaded,  0 extracted, (2 MB/? MB)
13:36:46.828 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  4 pending,  4 downloaded,  0 extracted, (4 MB/? MB)
13:36:46.885 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  4 pending,  4 downloaded,  1 extracted, (4 MB/? MB)
13:36:46.895 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  4 pending,  4 downloaded,  2 extracted, (4 MB/? MB)
13:36:46.902 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  4 pending,  4 downloaded,  3 extracted, (4 MB/? MB)
13:36:47.400 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  3 pending,  5 downloaded,  3 extracted, (5 MB/? MB)
13:36:50.222 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  2 pending,  6 downloaded,  3 extracted, (9 MB/? MB)
13:37:16.964 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  1 pending,  7 downloaded,  3 extracted, (88 MB/? MB)
13:37:18.064 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  1 pending,  7 downloaded,  4 extracted, (89 MB/? MB)
13:37:18.074 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  1 pending,  7 downloaded,  5 extracted, (89 MB/? MB)
13:37:18.081 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  1 pending,  7 downloaded,  6 extracted, (89 MB/? MB)
13:37:18.089 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  1 pending,  7 downloaded,  7 extracted, (89 MB/? MB)
13:37:18.097 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pulling image layers:  1 pending,  7 downloaded,  8 extracted, (89 MB/? MB)
13:37:18.106 [docker-java-stream-1567680985] INFO tc.postgres:alpine -- Pull complete. 8 layers, pulled in 34s (downloaded 89 MB at 2 MB/s)
13:37:18.111 [main] INFO tc.postgres:alpine -- Creating container for image: postgres:alpine
13:37:18.280 [main] INFO tc.postgres:alpine -- Container postgres:alpine is starting: de1c5c36fa428724411fda32a1d53051304d5001d630431dd0427957152c58e0
13:37:19.447 [main] INFO tc.postgres:alpine -- Container postgres:alpine started in PT38.594374044S
13:37:19.449 [main] INFO tc.postgres:alpine -- Container is started (JDBC URL: jdbc:postgresql://localhost:32773/test?loggerLevel=OFF)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.3)

2023-09-13T13:37:19.624+07:00  INFO 18120 --- [           main] b.t.TestBelajarTestcontainersApplication : Starting TestBelajarTestcontainersApplication using Java 17.0.8 with PID 18120 (started by rizki in /home/rizki/Documents/project/belajar-project/spring-project/belajar-testcontainers)
2023-09-13T13:37:19.625+07:00  INFO 18120 --- [           main] b.t.TestBelajarTestcontainersApplication : No active profile set, falling back to 1 default profile: "default"
2023-09-13T13:37:20.112+07:00  INFO 18120 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-09-13T13:37:20.150+07:00  INFO 18120 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 34 ms. Found 1 JPA repository interfaces.
2023-09-13T13:37:20.515+07:00  INFO 18120 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 0 (http)
2023-09-13T13:37:20.527+07:00  INFO 18120 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-09-13T13:37:20.527+07:00  INFO 18120 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.12]
2023-09-13T13:37:20.591+07:00  INFO 18120 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-09-13T13:37:20.593+07:00  INFO 18120 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 841 ms
2023-09-13T13:37:20.708+07:00  INFO 18120 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-09-13T13:37:20.733+07:00  INFO 18120 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.2.7.Final
2023-09-13T13:37:20.735+07:00  INFO 18120 --- [           main] org.hibernate.cfg.Environment            : HHH000406: Using bytecode reflection optimizer
2023-09-13T13:37:20.811+07:00  INFO 18120 --- [           main] o.h.b.i.BytecodeProviderInitiator        : HHH000021: Bytecode provider name : bytebuddy
2023-09-13T13:37:20.882+07:00  INFO 18120 --- [           main] o.s.o.j.p.SpringPersistenceUnitInfo      : No LoadTimeWeaver setup: ignoring JPA class transformer
2023-09-13T13:37:20.890+07:00  INFO 18120 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-09-13T13:37:20.989+07:00  INFO 18120 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@5dd6f517
2023-09-13T13:37:20.990+07:00  INFO 18120 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-09-13T13:37:21.107+07:00  INFO 18120 --- [           main] o.h.b.i.BytecodeProviderInitiator        : HHH000021: Bytecode provider name : bytebuddy
2023-09-13T13:37:21.420+07:00  INFO 18120 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: 
    drop table if exists tb_product cascade
2023-09-13T13:37:21.430+07:00  WARN 18120 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Warning Code: 0, SQLState: 00000
2023-09-13T13:37:21.430+07:00  WARN 18120 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : table "tb_product" does not exist, skipping
Hibernate: 
    create table tb_product (
        price numeric(38,2),
        quantity integer,
        id uuid not null,
        name varchar(255),
        primary key (id)
    )
2023-09-13T13:37:21.439+07:00  INFO 18120 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-09-13T13:37:21.612+07:00  WARN 18120 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-09-13T13:37:21.879+07:00  INFO 18120 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 36553 (http) with context path ''
2023-09-13T13:37:21.887+07:00  INFO 18120 --- [           main] b.t.TestBelajarTestcontainersApplication : Started TestBelajarTestcontainersApplication in 2.421 seconds (process running for 42.534)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
Hibernate: 
    insert 
    into
        tb_product
        (name,price,quantity,id) 
    values
        (?,?,?,?)
2023-09-13T13:37:22.878+07:00  INFO 18120 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-09-13T13:37:22.878+07:00  INFO 18120 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-09-13T13:37:22.881+07:00  INFO 18120 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 2 ms
Hibernate: 
    select
        p1_0.id,
        p1_0.name,
        p1_0.price,
        p1_0.quantity 
    from
        tb_product p1_0 
    where
        p1_0.id=?
Hibernate: 
    insert 
    into
        tb_product
        (name,price,quantity,id) 
    values
        (?,?,?,?)
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 44.121 s - in com.belajar.testcontainers.TestBelajarTestcontainersApplication
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  46.919 s
[INFO] Finished at: 2023-09-13T13:37:23+07:00
[INFO] ------------------------------------------------------------------------

Dari log diatas, dapat dilihat bahwa test dijalankan secara sukses dan dapat dilihat terdapat log dimana maven melakukan pull docker image lalu menjalankan testcontainer image tersebut dan kemudian unit test dapat dijalankans secara lancar.

Sekian artikel mengenai Belajar testcontainers, untuk source code diatas dapat anda akses di belajar testcontainers. Jika ada saran dan komentar silahkan isi dibawah dan terima kasih :).

Apa Itu CQRS ?

CQRS adalah kepanjangan dari Command Query Responsibility Segregation, dimana pattern ini dicetuskan oleh Greg Young. CQRS merupakah sebuah pattern yang menjelaskan bagaimana cara memisahkan antara proses menulis (command) dan membaca (query) pada sebuah aplikasi.

Jika kita melihat ke berbagai arsitektur microservice, masing - masing service dapat memiliki database nya tersendiri atau lebih sering disebut dengan database per service. Pembagian database per service juga dibagi ke dalam beberapa jenis yaitu

  1. Private-tables-per-service, yaitu semua service menggunakan database yang sama, akan tetapi setiap service hanya dapat mengakses tabel yang telah ditentukan.

  2. Schema-per-service, yaitu semua service memiliki database masing-masing akan tetapi database tersebut berada pada satu node database server.

  3. Database-server-per-service, yaitu semua service memiliki database masing-masing dan berada pada masing-masing node database server.

Berikut adalah contoh penggunaan database per service dengan menggunakan jenis Schema-per-service.

database-per-service.png

Dari contoh microservice diatas, jika pengguna ingin mengambil data order history, dimana kita harus menampilkan data order, data customer dan juga data product maka kita perlu melakukan call api terlebih dahulu ke service yang dibutuhkan lalu mengabungkannya, hal ini membutuhkan waktu yang lumayan panjang untuk menunggu response dari masing - masing service terlebih jika service yang dipanggil lebih dari 1.

Untuk menghadapi permasalah tersebut, Greg Young memberikan solusi berupa CQRS dimana proses untuk menulis (command) dan membaca (query) data dipisah sehingga service yang digunakan juga akan berbeda antara untuk pemrosesan data terkait create, update dan delete data. Misal seperti gambar arsitektur sebelum nya, maka untuk memunculkan data order history maka kita perlu membuat 1 service lagi khusus untuk menangani data order history tersebut.

Permasalahan lain juga muncul jika kita menggunakan database SQL yaitu para developer akan menambahkan indexing setiap column untuk mempercepat process pencarian data, akan tetapi penambahan indexing akan akan berimpact pada performance penyimpanan data, begitu juga sebalik nya. jika tidak ada indexing justru process pencarian data akan lambat jika data sudah sangat banyak tetapi process penyimpanan data semakin cepat.

Pada service yang bertugas sebagai query pada bagian CQRS juga mempunya database, dimana database pada service ini digunakan sebagai agregator untuk menyimpan data - data dari service yang diperlukan. Misalnya pada gambar arsitektur sebelum nya, maka kita membutuhkan table dan data yang berasal dari database customer dan product yang akan disimpan di database order juga. Perlu diingat bahwa database ini bisa berbeda antara service command dan service query.

Teknik Sinkronisasi Data

Pada CQRS, database yang digunakan pada service command dan query terpisah secara node nya. Database yang biasa digunakan pada service query adalah database yang dikhususkan untuk pencarian data secara cepat, contoh nya elasticsearch dan redis. Diperlukan teknik sinkronisasi data dari database pada service query dan service command sehingga data pada database service query selalu up to date. Ada 2 teknik untuk melakukan sinkronisasi data yaitu.

  1. Event base, dimana teknik ini menggunakan message broker sebagai pengiriman data dari service command ke service query. Teknik ini sangat dianjurkan karena process yang dilakukan secara asynchronous sehingga process command tidak terganggu secara performance.

  2. Change Data Capture (CDC), teknik ini mencapture atau tracking perubahan data pada sebuah database, jika ada perubahana maka dapat melakukan trigger action lain misal melakukan sinkronisasi ke database lain, bahkan mengirimkan data ke message broker dan rest api.

Pada artikel ini, penulis hanya akan membahas teknik event base dikarenakan teknik ini lebih banyak dipakai, lebih mudah digunakan dan secara processing lebih cepat. Berikut adalah arsitektur baru dengan menggunakan CQRS.

database-cqrs.png

Dari diagram diatas, dapat dilihat bahwa panah yang berwarna biru menunjukkan producer yang berfungsi untuk mengirimkan data ke message broker. Panah yang berwarna coklat berfungsi sebagai consumer. Sehingga flow nya akan berjalan seperti berikut.

  1. client melakukan order.
  2. request diterima oleh api gateway lalu diconvert menjadi grpc dan dikirimkan ke order command service.
  3. order command service melakukan process bisnis, jika berhasil maka data akan dikirim ke message broker secara asynchronous dan mengembalikan response ke api gateway.
  4. order query service menerima data baru dari message broker dan menyimpan nya ke database.
  5. client melihat order history.
  6. request diterima oleh api gateway lalu diconvert menjadi grpc dan dikirimkan ke order query service.
  7. order query service mengembalikan data history order.

Peringatan

Apakah CQRS wajib digunakan di semua yang berkaitan dengan query ? jawaban nya adalah tidak. Penggunaan CQRS lebih diutamakan untuk query yang cukup kompleks, service yang sering diakses oleh customer dan service yang sering dilakukan process pencarian.

Jangan pernah menggunakan scheduler untuk melakukan pooling data dari service - service command, karena ini nanti nya akan sangat berat jika data nya dalam jumlah yang banyak, susah di maintenance dan pasti nya tidak akan mendapatkan data yang up to date.

Sekian artikel mengenai Belajar cqrs, di artikel selanjutnya penulis akan membahas bagaimana cara membuat cqrs. Jika ada saran dan komentar silahkan isi dibawah dan terima kasih :).

Pada artikel instalasi perlengkapan coding golang, penulis sudah membahas mengenai bagaiman cara instalasi golang. Pada artikel ini, penulis akan membahas salah satu framework yang lumayan banyak digunakan yaitu gofiber atau biasa juga disebut fiber dan penulis akan memberikan contoh bagaimana penggunaan gofiber.

Apa itu GoFiber ?

GoFiber merupakan salah satu framework Go-Lang yang dibangun dengan Fasthttp dimana Fasthttp ini merupakan salah satu library atau package yang dibuat untuk keperluan server side.

GoFiber sendiri dikembangan berdasarkan inspirasi dari framework express js. Jadi bagi teman - teman yang pernah melakukan development dengan express js maka tidak akan asing lagi dengan framework gofiber ini.

Membuat Project

Untuk membuat project, silahkan buat sebuah folder yaitu belajar-gofiber lalu jalankan perintah

go mod init github.com/RizkiMufrizal/belajar-gofiber

Teman - teman bisa sesuaikan dengan nama project yang telah dibuat. Pada artikel ini, kita akan membuat restful api CRUD sederhana, pasti nya menggunakan ORM dari GORM. Untuk melakukan instalasi gofiber silahkan jalankan perintah berikut.

go get github.com/gofiber/fiber/v2

Lalu untuk gorm, silahkan jalankan perintah berikut.

go get -u gorm.io/gorm

Untuk driver mysql, silahkan jalankan perintah berikut.

go get -u gorm.io/driver/mysql

Lalu untuk konfigurasi .env, silahkan jalankan perintah berikut.

go get -u github.com/joho/godotenv

Standard Struktur Folder

Sebenarnya di dalam gofiber ini tidak ada suatu standard struktur folder melainkan dari masing - masing developer dapat menentukan standard nya.

➜  belajar-gofiber git:(master) ✗ tree -c
.
├── .env
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── README.md
├── go.mod
├── go.sum
├── main.go
├── configuration
├── controller
├── entity
├── middleware
├── model
├── exception
├── repository
│   └── impl
└── service
    └── impl
  1. .env : digunakan untuk menyimpan parameter seperti koneksi ke database, pooling dan sebagainya.
  2. .gitignore : digunakan untuk list file atau folder yang akan di ignore atau tidak ikut commit.
  3. Dockerfile : digunakan untuk konfigurasi docker.
  4. docker-compose.yml : digunakan untuk konfigurasi docker compose, biasa nya digunakan untuk menjalankan aplikasi pihak ketiga yang dibutuhkan oleh aplikasi kita seperti database, message broker dan lain sebagai nya.
  5. README.md : digunakan untuk informasi dari sebuah project.
  6. go.mod dan go.sum : adalah go module sebagai dependency management pada golang.
  7. main.go : digunakan untuk main file yang akan di jalankan di golang.
  8. configuration : digunakan untuk menyimpan konfigurasi seperti konfigurasi ke database, logger dan lain sebagain nya.
  9. controller : digunakan untuk menghadle request yang masuk ke aplikasi.
  10. entity : digunakan untuk mendefinisikan entity yang merepresentasikan ke table pada database.
  11. middleware : digunakan untuk processing yang berkaitan pada pre request maupun pre response. Biasa nya digunakan untuk pengecekan security, logging dan sebagainya.
  12. model : digunakan sebagai DTO (data transfer object), dimana biasa nya pada package ini terdapat object - object yang dibutuhkan untuk request dan response.
  13. exception : digunakan untuk kumpulan exception.
  14. repository : digunakan untuk akses ke database (repository pattern).
  15. repository/impl : digunakan untuk akses ke database (implementasi dari repository)
  16. service : digunakan untuk bisnis logic
  17. service/impl : digunakan untuk bisnis logic (implementasi dari service)

Membuat Konfigurasi

Untuk proses membuat konfigurasi ada beberapa tahapan yaitu

Membuat Konfigurasi .env

Pada file .env terdapat beberapa konfigurasi yaitu server port, database, database pooling dan user basic auth. Silahkan buka file .env lalu tambahkan code berikut

SERVER.PORT=0.0.0.0:9999

#Database Config
DATASOURCE_USERNAME=root
DATASOURCE_PASSWORD=root
DATASOURCE_HOST=localhost
DATASOURCE_PORT=3306
DATASOURCE_DB_NAME=belajar_gofiber

#Pool Config
DATASOURCE_POOL_MAX_CONN=10
DATASOURCE_POOL_IDLE_CONN=5
DATASOURCE_POOL_LIFE_TIME=30000

#User Basic Auth
username=admin
password=admin

Membuat Konfigurasi Pada Package configuration dan exception

Pada package exception, silahkan buat file error.go lalu masukkan code berikut

package exception

func PanicLogging(err interface{}) {
    if err != nil {
        panic(err)
    }
}

Code diatas berfungsi sebagai global error, jadi kita tidak perlu secara manual membuat panic, cukup panggil function tersebut.

Pada package configuration, silahkan buat 2 file yaitu config.go dan database.go. Silahkan buka file config.go lalu masukan konfigurasi seperti berikut.

package configuration

import (
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    "github.com/joho/godotenv"
    "os"
)

type Config interface {
    Get(key string) string
}

type configImpl struct {
}

func (config *configImpl) Get(key string) string {
    return os.Getenv(key)
}

func New(filenames ...string) Config {
    err := godotenv.Load(filenames...)
    exception.PanicLogging(err)
    return &configImpl{}
}

Code diatas berfungsi untuk memanggil file .env, sehingga kita bisa menggunakan parameter - parameter yang terdapat pada file .env.

Lalu pada file database.go silahkan isi code berikut.

package configuration

import (
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "log"
    "math/rand"
    "os"
    "strconv"
    "time"
)

func NewDatabase(config Config) *gorm.DB {
    username := config.Get("DATASOURCE_USERNAME")
    password := config.Get("DATASOURCE_PASSWORD")
    host := config.Get("DATASOURCE_HOST")
    port := config.Get("DATASOURCE_PORT")
    dbName := config.Get("DATASOURCE_DB_NAME")
    maxPoolOpen, err := strconv.Atoi(config.Get("DATASOURCE_POOL_MAX_CONN"))
    maxPoolIdle, err := strconv.Atoi(config.Get("DATASOURCE_POOL_IDLE_CONN"))
    maxPollLifeTime, err := strconv.Atoi(config.Get("DATASOURCE_POOL_LIFE_TIME"))
    exception.PanicLogging(err)

    loggerDb := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags),
        logger.Config{
            SlowThreshold:             time.Second,
            LogLevel:                  logger.Info,
            IgnoreRecordNotFoundError: true,
            Colorful:                  true,
        },
    )

    db, err := gorm.Open(mysql.Open(username+":"+password+"@tcp("+host+":"+port+")/"+dbName+"?parseTime=true"), &gorm.Config{
        Logger: loggerDb,
    })
    exception.PanicLogging(err)

    sqlDB, err := db.DB()
    exception.PanicLogging(err)

    sqlDB.SetMaxOpenConns(maxPoolOpen)
    sqlDB.SetMaxIdleConns(maxPoolIdle)
    sqlDB.SetConnMaxLifetime(time.Duration(rand.Int31n(int32(maxPollLifeTime))) * time.Millisecond)

    //autoMigrate
    return db
}

Pada bagian autoMigrate, nanti nya akan kita gunakan agar table nya otomatis di migrate sehingga kita tidak perlu create secara manual.

Membuat Entity dan Model

Pada bagian sebelum nya sudah dijelaskan bahwa entity akan berkaitan langsung dengan database sedangkan model akan berkaitan dengan DTO (data transfer object).

Membuat Entity

Pada artikel ini, kita hanya membuat 1 table saja yaitu table product, silahkan buat sebuah file product.go di dalam folder entity lalu masukkan code berikut.

package entity

type Product struct {
    Id       int32  `gorm:"primaryKey;auto_increment;column:product_id"`
    Name     string `gorm:"index;column:name;type:varchar(100)"`
    Price    int64  `gorm:"column:price"`
    Quantity int32  `gorm:"column:quantity"`
}

func (Product) TableName() string {
    return "tb_product"
}

Lalu silahkan buka file database.go kembali, pada bagian //autoMigrate silahkan tambahkan code berikut

//autoMigrate
err = db.AutoMigrate(&entity.Product{})
exception.PanicLogging(err)

Membuat Model

Pada package model, silahkan buat 1 buat file yaitu product_model.go lalu masukkan code berikut

package model

type ProductModel struct {
    Id       int32  `json:"id"`
    Name     string `json:"name"`
    Price    int64  `json:"price"`
    Quantity int32  `json:"quantity"`
}

type ProductCreateOrUpdateModel struct {
    Name     string `json:"name" validate:"required"`
    Price    int64  `json:"price" validate:"required"`
    Quantity int32  `json:"quantity" validate:"required"`
}

Dapat dilihat pada product model, kita mempunya 2 struct, dimana struct ProductModel digunakan untuk menampilkan product, sedangkan struct ProductCreateOrUpdateModel digunakan hanya untuk create dan update product saja. Lalu silkahkan buat sebuah file general_response.go untuk standarisasi response. lalu masukkan code berikut.

package model

type GeneralResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

Membuat Repository

Repository disini bertugas untuk mengakses ke database. Pada repository ini, kita akan membuat sebuah interface yang berfungsi sebagai mendeklarasikan function - function yang akan kita gunakan. Lalu interface ini nanti nya akan di implementasikan. Silahkan buat file product_repository.go pada package repository, lalu masukkan code berikut.

package repository

import (
    "context"
    "github.com/RizkiMufrizal/belajar-gofiber/entity"
)

type ProductRepository interface {
    Insert(ctx context.Context, product entity.Product) entity.Product
    Update(ctx context.Context, product entity.Product) entity.Product
    Delete(ctx context.Context, product entity.Product)
    FindById(ctx context.Context, id int32) (entity.Product, error)
    FindAl(ctx context.Context) []entity.Product
}

Lalu pada package repository/impl, silahkan buat file product_repository_impl.go lalu masukkan code berikut

package impl

import (
    "context"
    "errors"
    "github.com/RizkiMufrizal/belajar-gofiber/entity"
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    "github.com/RizkiMufrizal/belajar-gofiber/repository"
    "gorm.io/gorm"
)

func NewProductRepositoryImpl(DB *gorm.DB) repository.ProductRepository {
    return &productRepositoryImpl{DB: DB}
}

type productRepositoryImpl struct {
    *gorm.DB
}

func (repository *productRepositoryImpl) Insert(ctx context.Context, product entity.Product) entity.Product {
    err := repository.DB.WithContext(ctx).Create(&product).Error
    exception.PanicLogging(err)
    return product
}

func (repository *productRepositoryImpl) Update(ctx context.Context, product entity.Product) entity.Product {
    err := repository.DB.WithContext(ctx).Where("product_id = ?", product.Id).Updates(&product).Error
    exception.PanicLogging(err)
    return product
}

func (repository *productRepositoryImpl) Delete(ctx context.Context, product entity.Product) {
    err := repository.DB.WithContext(ctx).Delete(&product).Error
    exception.PanicLogging(err)
}

func (repository *productRepositoryImpl) FindById(ctx context.Context, id int32) (entity.Product, error) {
    var product entity.Product
    result := repository.DB.WithContext(ctx).Unscoped().Where("product_id = ?", id).First(&product)
    if result.RowsAffected == 0 {
        return entity.Product{}, errors.New("product Not Found")
    }
    return product, nil
}

func (repository *productRepositoryImpl) FindAl(ctx context.Context) []entity.Product {
    var products []entity.Product
    repository.DB.WithContext(ctx).Find(&products)
    return products
}

Membuat Service

Pada bagian package service, code yang akan kita tulis lebih ke fungsi logic bisnis yang diperlukan. Silahkan buat file product_service.go pada package service lalu masukkan code berikut.

package service

import (
    "context"
    "github.com/RizkiMufrizal/belajar-gofiber/model"
)

type ProductService interface {
    Create(ctx context.Context, model model.ProductCreateOrUpdateModel) model.ProductCreateOrUpdateModel
    Update(ctx context.Context, productModel model.ProductCreateOrUpdateModel, id int32) model.ProductCreateOrUpdateModel
    Delete(ctx context.Context, id int32)
    FindById(ctx context.Context, id int32) model.ProductModel
    FindAll(ctx context.Context) []model.ProductModel
}

Bisa dilihat dari code diatas, pada bagian service, kita akan menggunakan model, bukan entity untuk memunculkan atau menerima data product. Lalu buat sebuah file product_service_impl.go pada package service/impl, lalu masukkan code berikut

package impl

import (
    "context"
    "github.com/RizkiMufrizal/belajar-gofiber/entity"
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    "github.com/RizkiMufrizal/belajar-gofiber/model"
    "github.com/RizkiMufrizal/belajar-gofiber/repository"
    "github.com/RizkiMufrizal/belajar-gofiber/service"
)

func NewProductServiceImpl(productRepository *repository.ProductRepository) service.ProductService {
    return &productServiceImpl{ProductRepository: *productRepository}
}

type productServiceImpl struct {
    repository.ProductRepository
}

func (service *productServiceImpl) Create(ctx context.Context, productModel model.ProductCreateOrUpdateModel) model.ProductCreateOrUpdateModel {
    product := entity.Product{
        Name:     productModel.Name,
        Price:    productModel.Price,
        Quantity: productModel.Quantity,
    }
    service.ProductRepository.Insert(ctx, product)
    return productModel
}

func (service *productServiceImpl) Update(ctx context.Context, productModel model.ProductCreateOrUpdateModel, id int32) model.ProductCreateOrUpdateModel {
    product := entity.Product{
        Id:       id,
        Name:     productModel.Name,
        Price:    productModel.Price,
        Quantity: productModel.Quantity,
    }
    service.ProductRepository.Update(ctx, product)
    return productModel
}

func (service *productServiceImpl) Delete(ctx context.Context, id int32) {
    product, err := service.ProductRepository.FindById(ctx, id)
    if err != nil {
        panic(exception.NotFoundError{
            Message: err.Error(),
        })
    }
    service.ProductRepository.Delete(ctx, product)
}

func (service *productServiceImpl) FindById(ctx context.Context, id int32) model.ProductModel {
    product, err := service.ProductRepository.FindById(ctx, id)
    exception.PanicLogging(err)

    return model.ProductModel{
        Id:       product.Id,
        Name:     product.Name,
        Price:    product.Price,
        Quantity: product.Quantity,
    }
}

func (service *productServiceImpl) FindAll(ctx context.Context) (responses []model.ProductModel) {
    products := service.ProductRepository.FindAl(ctx)
    for _, product := range products {
        responses = append(responses, model.ProductModel{
            Id:       product.Id,
            Name:     product.Name,
            Price:    product.Price,
            Quantity: product.Quantity,
        })
    }
    if len(products) == 0 {
        return []model.ProductModel{}
    }
    return responses
}

Pada bagian exception.NotFoundError pasti error karena kita belum membuat error handling tersebut, silahkan buat file not_found_error.go pada package exception lalu masukkan code berikut.

package exception

type NotFoundError struct {
    Message string
}

func (notFoundError NotFoundError) Error() string {
    return notFoundError.Message
}

Membuat Middleware

Sebelum lanjut ke controller, kita akan membuat middleware pada artikel ini. Middleware yang dibuat pada artikel disini hanya sebatas security dengan menggunakan basic authentication dan credentials nya sementara akan di hardcode di file .env. Silahkan buat file basic_auth.go di dalam package middleware lalu masukkan code berikut.

package middleware

import (
    "encoding/base64"
    "github.com/RizkiMufrizal/belajar-gofiber/configuration"
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    "github.com/RizkiMufrizal/belajar-gofiber/model"
    "github.com/gofiber/fiber/v2"
    "strings"
)

func BasicAuth(config configuration.Config) fiber.Handler {
    return func(ctx *fiber.Ctx) error {
        username := config.Get("USERNAME")
        password := config.Get("PASSWORD")

        basicAuth := ctx.Get("Authorization")

        if basicAuth == "" {
            return ctx.
                Status(fiber.StatusBadRequest).
                JSON(model.GeneralResponse{
                    Code:    404,
                    Message: "Bad Request",
                    Data:    "Header Not Found",
                })
        }

        basicAuthDecode, err := base64.StdEncoding.DecodeString(strings.Split(basicAuth, " ")[1])
        exception.PanicLogging(err)
        basicAuthDecodeString := string(basicAuthDecode)
        basicAuthUsername := strings.Split(basicAuthDecodeString, ":")[0]
        basicAuthPassword := strings.Split(basicAuthDecodeString, ":")[1]

        if username != basicAuthUsername && password != basicAuthPassword {
            return ctx.
                Status(fiber.StatusUnauthorized).
                JSON(model.GeneralResponse{
                    Code:    401,
                    Message: "Unauthorized",
                    Data:    "Invalid Credentials",
                })
        }

        return ctx.Next()
    }
}

Membuat Controller

Semua yang terdapat di dalam controller akan menghadle request yang datang dari user / client. Silahkan buat file product_controller.go pada package controller, lalu masukkan code berikut.

package controller

import (
    "github.com/RizkiMufrizal/belajar-gofiber/configuration"
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    "github.com/RizkiMufrizal/belajar-gofiber/middleware"
    "github.com/RizkiMufrizal/belajar-gofiber/model"
    "github.com/RizkiMufrizal/belajar-gofiber/service"
    "github.com/gofiber/fiber/v2"
)

type ProductController struct {
    service.ProductService
    configuration.Config
}

func NewProductController(productService *service.ProductService, config configuration.Config) *ProductController {
    return &ProductController{ProductService: *productService, Config: config}
}

func (controller ProductController) Route(app *fiber.App) {
    app.Post("/v1/api/product", middleware.BasicAuth(controller.Config), controller.Create)
    app.Put("/v1/api/product/:id", middleware.BasicAuth(controller.Config), controller.Update)
    app.Delete("/v1/api/product/:id", middleware.BasicAuth(controller.Config), controller.Delete)
    app.Get("/v1/api/product/:id", middleware.BasicAuth(controller.Config), controller.FindById)
    app.Get("/v1/api/product", middleware.BasicAuth(controller.Config), controller.FindAll)
}

func (controller ProductController) Create(c *fiber.Ctx) error {
    var request model.ProductCreateOrUpdateModel
    err := c.BodyParser(&request)
    exception.PanicLogging(err)

    response := controller.ProductService.Create(c.Context(), request)
    return c.Status(fiber.StatusCreated).JSON(model.GeneralResponse{
        Code:    200,
        Message: "Success",
        Data:    response,
    })
}

func (controller ProductController) Update(c *fiber.Ctx) error {
    var request model.ProductCreateOrUpdateModel
    id, err := c.ParamsInt("id")
    err = c.BodyParser(&request)
    exception.PanicLogging(err)

    response := controller.ProductService.Update(c.Context(), request, int32(id))
    return c.Status(fiber.StatusOK).JSON(model.GeneralResponse{
        Code:    200,
        Message: "Success",
        Data:    response,
    })
}

func (controller ProductController) Delete(c *fiber.Ctx) error {
    id, err := c.ParamsInt("id")
    exception.PanicLogging(err)

    controller.ProductService.Delete(c.Context(), int32(id))
    return c.Status(fiber.StatusOK).JSON(model.GeneralResponse{
        Code:    200,
        Message: "Success",
    })
}

func (controller ProductController) FindById(c *fiber.Ctx) error {
    id, err := c.ParamsInt("id")
    exception.PanicLogging(err)

    result := controller.ProductService.FindById(c.Context(), int32(id))
    return c.Status(fiber.StatusOK).JSON(model.GeneralResponse{
        Code:    200,
        Message: "Success",
        Data:    result,
    })
}

func (controller ProductController) FindAll(c *fiber.Ctx) error {
    result := controller.ProductService.FindAll(c.Context())
    return c.Status(fiber.StatusOK).JSON(model.GeneralResponse{
        Code:    200,
        Message: "Success",
        Data:    result,
    })
}

Membuat Exception Handler

Yang tidak kalah penting nya adalah membuat exception handler, Dimana exception ini agar aplikasi tidak berhenti dan dapat mengeluarkan error secara proper. Silahkan buat file error_handler.go pada package exception lalu isi code berikut

package exception

import (
    "github.com/RizkiMufrizal/belajar-gofiber/model"
    "github.com/gofiber/fiber/v2"
)

func ErrorHandler(ctx *fiber.Ctx, err error) error {
    _, notFoundError := err.(NotFoundError)
    if notFoundError {
        return ctx.Status(fiber.StatusNotFound).JSON(model.GeneralResponse{
            Code:    404,
            Message: "Not Found",
            Data:    err.Error(),
        })
    }

    return ctx.Status(fiber.StatusInternalServerError).JSON(model.GeneralResponse{
        Code:    500,
        Message: "General Error",
        Data:    err.Error(),
    })
}

Lalu buat sebuah file fiber.go di dalam configuration lalu isi code berikut

package configuration

import (
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    "github.com/gofiber/fiber/v2"
)

func NewFiberConfiguration() fiber.Config {
    return fiber.Config{
        ErrorHandler: exception.ErrorHandler,
    }
}

Code pada fiber.go nanti nya akan dipanggil senagai error handling, sehingga diharapkan aplikasi dapat mengeluarkan response code yang proper.

Membuat Main

Silahkan buat file main.go di root folder, file ini bertugas sebagai main object yang akan dijalankan. Silahkan masukkan code nya seperti berikut.

package main

import (
    "github.com/RizkiMufrizal/belajar-gofiber/configuration"
    "github.com/RizkiMufrizal/belajar-gofiber/controller"
    "github.com/RizkiMufrizal/belajar-gofiber/exception"
    repository "github.com/RizkiMufrizal/belajar-gofiber/repository/impl"
    service "github.com/RizkiMufrizal/belajar-gofiber/service/impl"
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/recover"
)

func main() {
    //setup configuration
    config := configuration.New()
    database := configuration.NewDatabase(config)

    //repository
    productRepository := repository.NewProductRepositoryImpl(database)

    //service
    productService := service.NewProductServiceImpl(&productRepository)

    //controller
    productController := controller.NewProductController(&productService, config)

    //setup fiber
    app := fiber.New(configuration.NewFiberConfiguration())
    app.Use(recover.New())
    app.Use(cors.New())

    //routing
    productController.Route(app)

    //start app
    err := app.Listen(config.Get("SERVER.PORT"))
    exception.PanicLogging(err)
}

Membuat Dockerfile dan Docker Compose

Pada tahap terakhir, kita perlu membuat dockerfile jika nanti suatu saat perlu di deploy ke kubernetes atau openshift. Silahkan buat file Dockerfile di root folder lalu masukkan code berikut.

# Get Go image from DockerHub.
FROM golang:1.19.5 AS api

# Set working directory.
WORKDIR /compiler

# Copy dependency locks so we can cache.
COPY go.mod go.sum ./

# Get all of our dependencies.
RUN go mod download

# Copy all of our remaining application.
COPY . .

# Build our application.
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./main.go

# Use 'scratch' image for super-mini build.
FROM scratch AS prod

# Set working directory for this stage.
WORKDIR /app

# Copy our compiled executable from the last stage.
COPY --from=api /compiler/server .

# Run application and expose port 9999.
EXPOSE 9999
CMD ["./server"]

Untuk proses build docker image, teman - teman bisa lihat cara nya di belajar docker.

Sedangkan untuk docker compose, silahkan buat file docker-compose.yml pada root folder lalu masukkan code berikut

version: '3.1'

services:

  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: belajar_gofiber
    ports:
      - "3306:3306"

  adminer:
    image: adminer
    restart: always
    ports:
      - "8080:8080"

Lalu jalankan docker compose dengan perintah

docker compose up

Menjalankan, Build dan Test Aplikasi

Setelah selesai membuat code nya, tahapan selanjut nya adalah menjalankan aplikasi nya. Cara menjalankan silahkan gunakan perintah berikut.

go run ./main.go

Jika sudah muncul output berikut

┌───────────────────────────────────────────────────┐ 
│                   Fiber v2.41.0                   │ 
│               http://127.0.0.1:9999               │ 
│       (bound on host 0.0.0.0 and port 9999)       │ 
│                                                   │ 
│ Handlers ............ 16  Processes ........... 1 │ 
│ Prefork ....... Disabled  PID ............. 32394 │ 
└───────────────────────────────────────────────────┘

Berarti aplikasi gofiber nya sudah berjalan sebagai mesti nya. Untuk melakukan build, teman - teman cukup menjalankan perintah berikut.

go build ./main.go

Untuk melakukan test aplikasi, teman - teman bisa menggunakan postman atau perintah curl. Contoh nya disini penulis menggunakan perintah curl seperti berikut

curl --location --request POST 'http://localhost:9999/v1/api/product' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "product 1",
    "price": 1000,
    "quantity": 5
}'

dan hasil nya

{"code":200,"message":"Success","data":{"name":"product 1","price":1000,"quantity":5}}

Atau jika teman - teman ingin menampilkan semua nya bisa menggunakan perintah berikut.

curl --location --request GET 'http://localhost:9999/v1/api/product' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--header 'Content-Type: application/json'

dan berikut hasil nya

{"code":200,"message":"Success","data":[{"id":1,"name":"product 1","price":1000,"quantity":5}]}

Akhirnya selesai juga artikel mengenai belajar go fiber :). Untuk source code diatas dapat anda akses di belajar gofiber atau jika teman - teman ingin yang lebih advance bisa lihat source nya di gofiber clean architecture). Sekian artikel mengenai Belajar gofiber, jika ada saran dan komentar silahkan isi dibawah dan terima kasih :).