Dynamic Query Builder
Pustaka Go yang powerful untuk membangun query SQL dan MongoDB secara dinamis dengan dukungan filtering, sorting, pagination, join, CTE, window functions, dan operasi JSON/Array.
Fitur Utama
- Multi-Database: Mendukung PostgreSQL, MySQL, SQLite, SQL Server, dan MongoDB
- Dynamic Filtering: Berbagai operator filter (_eq, _neq, _like, _in, _between, dll.)
- JSON Operations: Dukungan penuh untuk query dan update data JSON
- Array Operations: Query array dengan berbagai operator
- Security: Built-in proteksi SQL injection dan kontrol akses kolom/tabel
- Window Functions: Dukungan ROW_NUMBER, RANK, dll.
- CTE & Unions: Support untuk Common Table Expressions dan UNION
- Query Parsing: Parse URL query parameters menjadi DynamicQuery
Instalasi
go get github.com/Masterminds/squirrel
go get github.com/jmoiron/sqlx
go get go.mongodb.org/mongo-driver/mongo
Contoh Penggunaan
1. Operator Perbandingan Dasar
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
func main() {
// Inisialisasi QueryBuilder
qb := NewQueryBuilder(DBTypePostgreSQL)
// Koneksi ke database
db, err := sqlx.Connect("postgres", "user=postgres dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Equal (_eq)
query := DynamicQuery{
From: "users",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "email",
Operator: OpEqual,
Value: "john@example.com",
}},
LogicOp: "AND",
}},
}
var users []map[string]interface{}
err = qb.ExecuteQuery(context.Background(), db, query, &users)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d users\n", len(users))
// Greater Than (_gt)
query = DynamicQuery{
From: "products",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "price",
Operator: OpGreaterThan,
Value: 100000,
}},
LogicOp: "AND",
}},
Limit: 10,
}
var products []map[string]interface{}
err = qb.ExecuteQuery(context.Background(), db, query, &products)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d products with price > 100000\n", len(products))
}
2. Operator Like dan String Matching
func stringMatchingExample(qb *QueryBuilder, db *sqlx.DB) {
// Case Insensitive Like (_ilike)
query := DynamicQuery{
From: "products",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "name",
Operator: OpILike,
Value: "%laptop%",
}},
LogicOp: "AND",
}},
Limit: 20,
}
var products []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &products)
// Contains (_contains)
query = DynamicQuery{
From: "articles",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "content",
Operator: OpContains,
Value: "golang",
}},
LogicOp: "AND",
}},
}
var articles []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &articles)
// Starts With (_starts_with)
query = DynamicQuery{
From: "users",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "username",
Operator: OpStartsWith,
Value: "admin",
}},
LogicOp: "AND",
}},
}
var admins []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &admins)
}
3. Operator IN, NOT IN, dan Between
func inBetweenExample(qb *QueryBuilder, db *sqlx.DB) {
// In (_in)
query := DynamicQuery{
From: "products",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "category",
Operator: OpIn,
Value: []string{"electronics", "computers", "gadgets"},
}},
LogicOp: "AND",
}},
Limit: 50,
}
var products []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &products)
// Between (_between)
query = DynamicQuery{
From: "orders",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "order_date",
Operator: OpBetween,
Value: []interface{}{time.Now().AddDate(0, -1, 0), time.Now()},
}},
LogicOp: "AND",
}},
Sort: []SortField{{Column: "order_date", Order: "DESC"}},
Limit: 100,
}
var orders []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &orders)
}
4. Operator Null dan Exists
func nullExistsExample(qb *QueryBuilder, db *sqlx.DB) {
// Is Null (_null)
query := DynamicQuery{
From: "users",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "deleted_at",
Operator: OpNull,
Value: nil,
}},
LogicOp: "AND",
}},
}
var activeUsers []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &activeUsers)
// Is Not Null (_nnull)
query = DynamicQuery{
From: "products",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "description",
Operator: OpNotNull,
Value: nil,
}},
LogicOp: "AND",
}},
}
var productsWithDesc []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &productsWithDesc)
}
5. Operator JSON
func jsonExample(qb *QueryBuilder, db *sqlx.DB) {
// JSON Contains (_json_contains)
query := DynamicQuery{
From: "products",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "attributes",
Operator: OpJsonContains,
Value: `{"color": "red"}`,
}},
LogicOp: "AND",
}},
}
var redProducts []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &redProducts)
// JSON Exists (_json_exists)
query = DynamicQuery{
From: "users",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "profile",
Operator: OpJsonExists,
Options: map[string]interface{}{
"path": "$.social_media",
},
}},
LogicOp: "AND",
}},
}
var usersWithSocialMedia []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &usersWithSocialMedia)
// JSON Equal (_json_eq)
query = DynamicQuery{
From: "settings",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "preferences",
Operator: OpJsonEqual,
Value: "dark_mode",
Options: map[string]interface{}{
"path": "$.theme",
},
}},
LogicOp: "AND",
}},
}
var darkModeUsers []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &darkModeUsers)
}
6. Operator Array
func arrayExample(qb *QueryBuilder, db *sqlx.DB) {
// Array Contains (_array_contains)
query := DynamicQuery{
From: "products",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "tags",
Operator: OpArrayContains,
Value: "premium",
}},
LogicOp: "AND",
}},
}
var premiumProducts []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &premiumProducts)
// Array Length (_array_length)
query = DynamicQuery{
From: "galleries",
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "images",
Operator: OpArrayLength,
Options: map[string]interface{}{
"length": 5,
},
}},
LogicOp: "AND",
}},
}
var galleriesWith5Images []map[string]interface{}
qb.ExecuteQuery(context.Background(), db, query, &galleriesWith5Images)
}
7. Complex Query dengan Multiple Filters
func complexQueryExample(qb *QueryBuilder, db *sqlx.DB) {
// Query kompleks dengan multiple filter groups
query := DynamicQuery{
From: "products",
Fields: []SelectField{
{Expression: "id", Alias: "product_id"},
{Expression: "name", Alias: "product_name"},
{Expression: "price", Alias: "price"},
{Expression: "category", Alias: "category"},
},
Filters: []FilterGroup{
// Filter Group 1: Active products with price range
{
Filters: []DynamicFilter{
{
Column: "status",
Operator: OpEqual,
Value: "active",
},
{
Column: "price",
Operator: OpBetween,
Value: []interface{}{100000, 500000},
},
},
LogicOp: "AND",
},
// Filter Group 2: Category OR name matching
{
Filters: []DynamicFilter{
{
Column: "category",
Operator: OpIn,
Value: []string{"electronics", "computers"},
},
{
Column: "name",
Operator: OpILike,
Value: "%laptop%",
},
},
LogicOp: "OR",
},
},
Sort: []SortField{
{Column: "price", Order: "ASC"},
{Column: "name", Order: "ASC"},
},
Limit: 20,
Offset: 0,
}
var results []map[string]interface{}
err := qb.ExecuteQuery(context.Background(), db, query, &results)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d products\n", len(results))
for _, result := range results {
fmt.Printf("Product: %s, Price: %v\n", result["product_name"], result["price"])
}
}
8. Query dengan JOIN
func joinExample(qb *QueryBuilder, db *sqlx.DB) {
query := DynamicQuery{
From: "orders",
Aliases: "o",
Fields: []SelectField{
{Expression: "o.id", Alias: "order_id"},
{Expression: "o.order_date", Alias: "order_date"},
{Expression: "c.name", Alias: "customer_name"},
{Expression: "p.name", Alias: "product_name"},
},
Joins: []Join{
{
Type: "INNER",
Table: "customers",
Alias: "c",
OnConditions: FilterGroup{
Filters: []DynamicFilter{
{
Column: "o.customer_id",
Operator: OpEqual,
Value: "c.id",
},
},
LogicOp: "AND",
},
},
{
Type: "LEFT",
Table: "products",
Alias: "p",
OnConditions: FilterGroup{
Filters: []DynamicFilter{
{
Column: "o.product_id",
Operator: OpEqual,
Value: "p.id",
},
},
LogicOp: "AND",
},
},
},
Filters: []FilterGroup{{
Filters: []DynamicFilter{
{
Column: "o.order_date",
Operator: OpGreaterThanEqual,
Value: time.Now().AddDate(0, -1, 0),
},
},
LogicOp: "AND",
}},
Sort: []SortField{
{Column: "o.order_date", Order: "DESC"},
},
Limit: 50,
}
var orders []map[string]interface{}
err := qb.ExecuteQuery(context.Background(), db, query, &orders)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d orders\n", len(orders))
}
9. Query dengan Window Functions
func windowFunctionExample(qb *QueryBuilder, db *sqlx.DB) {
query := DynamicQuery{
From: "sales",
Fields: []SelectField{
{Expression: "id", Alias: "sale_id"},
{Expression: "product_name", Alias: "product"},
{Expression: "amount", Alias: "sale_amount"},
},
WindowFunctions: []WindowFunction{
{
Function: "ROW_NUMBER",
Over: "product_name",
OrderBy: "amount DESC",
Alias: "rank_in_category",
},
{
Function: "SUM",
Over: "product_name",
OrderBy: "sale_date",
Frame: "ROWS UNBOUNDED PRECEDING",
Alias: "running_total",
},
},
Sort: []SortField{
{Column: "product_name", Order: "ASC"},
{Column: "amount", Order: "DESC"},
},
}
var sales []map[string]interface{}
err := qb.ExecuteQuery(context.Background(), db, query, &sales)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d sales records\n", len(sales))
}
10. MongoDB Example
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func mongodbExample() {
// Inisialisasi MongoQueryBuilder
mqb := NewMongoQueryBuilder()
// Koneksi ke MongoDB
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
defer client.Disconnect(context.Background())
collection := client.Database("mydb").Collection("products")
// Query dengan filter
query := DynamicQuery{
From: "products",
Filters: []FilterGroup{
{
Filters: []DynamicFilter{
{
Column: "status",
Operator: OpEqual,
Value: "active",
},
{
Column: "price",
Operator: OpBetween,
Value: []interface{}{100000, 500000},
},
},
LogicOp: "AND",
},
{
Filters: []DynamicFilter{
{
Column: "category",
Operator: OpIn,
Value: []string{"electronics", "computers"},
},
{
Column: "name",
Operator: OpILike,
Value: "laptop",
},
},
LogicOp: "OR",
},
},
Sort: []SortField{
{Column: "price", Order: "ASC"},
{Column: "name", Order: "ASC"},
},
Limit: 20,
Offset: 0,
}
var results []map[string]interface{}
err = mqb.ExecuteFind(context.Background(), collection, query, &results)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d products\n", len(results))
for _, result := range results {
fmt.Printf("Product: %s, Price: %v\n", result["name"], result["price"])
}
}
11. URL Query Parsing
import (
"net/url"
)
func urlParsingExample() {
// Parse URL query parameters
values, _ := url.ParseQuery("filter[status][_eq]=active&filter[price][_gte]=100000&sort=-created_at&limit=20")
parser := NewQueryParser()
query, err := parser.ParseQuery(values, "products")
if err != nil {
log.Fatal(err)
}
// Query akan berisi:
// - Filters: status = 'active' AND price >= 100000
// - Sort: created_at DESC
// - Limit: 20
qb := NewQueryBuilder(DBTypePostgreSQL)
db, _ := sqlx.Connect("postgres", "connection-string")
var products []map[string]interface{}
err = qb.ExecuteQuery(context.Background(), db, query, &products)
if err != nil {
log.Fatal(err)
}
}
Konfigurasi Keamanan
func securityExample() {
qb := NewQueryBuilder(DBTypePostgreSQL)
// Aktifkan security checks
qb.SetSecurityOptions(true, 1000) // max 1000 rows
// Tentukan kolom yang diizinkan
qb.SetAllowedColumns([]string{
"id", "name", "email", "status", "created_at",
"price", "category", "description",
})
// Tentukan tabel yang diizinkan
qb.SetAllowedTables([]string{
"users", "products", "orders", "categories",
})
// Query dengan kolom/tabel tidak diizinkan akan error
query := DynamicQuery{
From: "users", // OK
Filters: []FilterGroup{{
Filters: []DynamicFilter{{
Column: "password", // ERROR: kolom tidak diizinkan
Operator: OpEqual,
Value: "secret",
}},
LogicOp: "AND",
}},
}
// Akan mengembalikan error
_, _, err := qb.BuildQuery(query)
fmt.Println(err) // "disallowed column: password"
}
Referensi Operator
Operator Perbandingan
_eq- Equal_neq- Not Equal_gt- Greater Than_gte- Greater Than Equal_lt- Less Than_lte- Less Than Equal
Operator String
_like- Like (case-sensitive)_ilike- Like (case-insensitive)_nlike- Not Like (case-sensitive)_nilike- Not Like (case-insensitive)_contains- Contains substring_ncontains- Not Contains substring_starts_with- Starts with_ends_with- Ends with
Operator Set
_in- In list_nin- Not In list_between- Between two values_nbetween- Not Between two values
Operator Null
_null- Is Null_nnull- Is Not Null
Operator JSON
_json_contains- JSON contains value_json_ncontains- JSON not contains value_json_exists- JSON path exists_json_nexists- JSON path not exists_json_eq- JSON path equals value_json_neq- JSON path not equals value
Operator Array
_array_contains- Array contains value_array_ncontains- Array not contains value_array_length- Array has specific length
Best Practices
-
Selalu gunakan security checks saat production:
qb.SetSecurityOptions(true, 1000) qb.SetAllowedColumns(allowedColumns) qb.SetAllowedTables(allowedTables) -
Gunakan context dengan timeout untuk query:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() -
Validasi input user sebelum membuat DynamicFilter
-
Gunakan prepared statements (sudah otomatis di QueryBuilder)
-
Log query untuk debugging:
qb.SetQueryLogging(true)
Kontribusi
Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
License
MIT License