Query Object (Объект-запрос)

Паттерн проектирования Query Object

Паттерн проектирования Query Object

Описание Query Object

Объект, представляющий запрос к БД

SQL зачастую сопряженно используемый язык и многие разработчики не совсем хорошо в нём разбираются. Более того, необходимо знать, как выглядит структура базы данных, чтобы создавать запросы. Можно избежать этого, создав специальные методы поиска, которые сожержат в себе весь SQL и управляются ограниченным набором параметров. Такой подход делает сложным создание и тюнинг запросов для конкретных ситуаций. Также это ведёт к дублированию в SQL-выражениях.

Паттерн Query Object - это структура объектов, которая может интерпретироваться в SQL-запрос. Можно создавать такой запрос ссылаясь на классы и поля так же как на таблицы и столбцы. Таким образом создаётся независимость разработчика от струткуры БД и конкретной реализации БД.

Примеры реализации

// Query Object Pattern in JavaScript
class QueryObject {
    constructor() {
        this.selectFields = [];
        this.fromTable = '';
        this.whereConditions = [];
        this.orderByFields = [];
        this.limitCount = null;
        this.offsetCount = null;
    }
    
    select(fields) {
        this.selectFields = Array.isArray(fields) ? fields : [fields];
        return this;
    }
    
    from(table) {
        this.fromTable = table;
        return this;
    }
    
    where(condition) {
        this.whereConditions.push(condition);
        return this;
    }
    
    orderBy(field, direction = 'ASC') {
        this.orderByFields.push({ field, direction });
        return this;
    }
    
    limit(count) {
        this.limitCount = count;
        return this;
    }
    
    offset(count) {
        this.offsetCount = count;
        return this;
    }
    
    toSQL() {
        let sql = 'SELECT ';
        
        if (this.selectFields.length === 0) {
            sql += '*';
        } else {
            sql += this.selectFields.join(', ');
        }
        
        sql += ` FROM ${this.fromTable}`;
        
        if (this.whereConditions.length > 0) {
            sql += ' WHERE ' + this.whereConditions.join(' AND ');
        }
        
        if (this.orderByFields.length > 0) {
            const orderClauses = this.orderByFields.map(item => 
                `${item.field} ${item.direction}`
            );
            sql += ' ORDER BY ' + orderClauses.join(', ');
        }
        
        if (this.limitCount !== null) {
            sql += ` LIMIT ${this.limitCount}`;
        }
        
        if (this.offsetCount !== null) {
            sql += ` OFFSET ${this.offsetCount}`;
        }
        
        return sql;
    }
}

// Usage
const query = new QueryObject()
    .select(['id', 'name', 'email'])
    .from('users')
    .where('active = 1')
    .orderBy('name', 'ASC')
    .limit(10);

console.log('Query SQL:', query.toSQL());
<?php
// Query Object Pattern in PHP
class QueryObject {
    private $selectFields = [];
    private $fromTable = '';
    private $whereConditions = [];
    private $orderByFields = [];
    private $limitCount = null;
    private $offsetCount = null;
    
    public function select($fields) {
        $this->selectFields = is_array($fields) ? $fields : [$fields];
        return $this;
    }
    
    public function from($table) {
        $this->fromTable = $table;
        return $this;
    }
    
    public function where($condition) {
        $this->whereConditions[] = $condition;
        return $this;
    }
    
    public function orderBy($field, $direction = 'ASC') {
        $this->orderByFields[] = ['field' => $field, 'direction' => $direction];
        return $this;
    }
    
    public function limit($count) {
        $this->limitCount = $count;
        return $this;
    }
    
    public function offset($count) {
        $this->offsetCount = $count;
        return $this;
    }
    
    public function toSQL() {
        $sql = 'SELECT ';
        
        if (empty($this->selectFields)) {
            $sql .= '*';
        } else {
            $sql .= implode(', ', $this->selectFields);
        }
        
        $sql .= " FROM {$this->fromTable}";
        
        if (!empty($this->whereConditions)) {
            $sql .= ' WHERE ' . implode(' AND ', $this->whereConditions);
        }
        
        if (!empty($this->orderByFields)) {
            $orderClauses = array_map(function($item) {
                return "{$item['field']} {$item['direction']}";
            }, $this->orderByFields);
            $sql .= ' ORDER BY ' . implode(', ', $orderClauses);
        }
        
        if ($this->limitCount !== null) {
            $sql .= " LIMIT {$this->limitCount}";
        }
        
        if ($this->offsetCount !== null) {
            $sql .= " OFFSET {$this->offsetCount}";
        }
        
        return $sql;
    }
}

// Usage
$query = (new QueryObject())
    ->select(['id', 'name', 'email'])
    ->from('users')
    ->where('active = 1')
    ->orderBy('name', 'ASC')
    ->limit(10);

echo "Query SQL: " . $query->toSQL() . "\n";
?>
// Query Object Pattern in Go
package main

import (
    "fmt"
    "strings"
)

type QueryObject struct {
    selectFields   []string
    fromTable     string
    whereConditions []string
    orderByFields []OrderByField
    limitCount    *int
    offsetCount   *int
}

type OrderByField struct {
    Field     string
    Direction string
}

func NewQueryObject() *QueryObject {
    return &QueryObject{
        selectFields:    []string{},
        whereConditions: []string{},
        orderByFields:   []OrderByField{},
    }
}

func (q *QueryObject) Select(fields ...string) *QueryObject {
    q.selectFields = fields
    return q
}

func (q *QueryObject) From(table string) *QueryObject {
    q.fromTable = table
    return q
}

func (q *QueryObject) Where(condition string) *QueryObject {
    q.whereConditions = append(q.whereConditions, condition)
    return q
}

func (q *QueryObject) OrderBy(field, direction string) *QueryObject {
    if direction == "" {
        direction = "ASC"
    }
    q.orderByFields = append(q.orderByFields, OrderByField{
        Field:     field,
        Direction: direction,
    })
    return q
}

func (q *QueryObject) Limit(count int) *QueryObject {
    q.limitCount = &count
    return q
}

func (q *QueryObject) Offset(count int) *QueryObject {
    q.offsetCount = &count
    return q
}

func (q *QueryObject) ToSQL() string {
    var sql strings.Builder
    
    sql.WriteString("SELECT ")
    if len(q.selectFields) == 0 {
        sql.WriteString("*")
    } else {
        sql.WriteString(strings.Join(q.selectFields, ", "))
    }
    
    sql.WriteString(" FROM ")
    sql.WriteString(q.fromTable)
    
    if len(q.whereConditions) > 0 {
        sql.WriteString(" WHERE ")
        sql.WriteString(strings.Join(q.whereConditions, " AND "))
    }
    
    if len(q.orderByFields) > 0 {
        sql.WriteString(" ORDER BY ")
        orderClauses := make([]string, len(q.orderByFields))
        for i, field := range q.orderByFields {
            orderClauses[i] = fmt.Sprintf("%s %s", field.Field, field.Direction)
        }
        sql.WriteString(strings.Join(orderClauses, ", "))
    }
    
    if q.limitCount != nil {
        sql.WriteString(fmt.Sprintf(" LIMIT %d", *q.limitCount))
    }
    
    if q.offsetCount != nil {
        sql.WriteString(fmt.Sprintf(" OFFSET %d", *q.offsetCount))
    }
    
    return sql.String()
}

func main() {
    query := NewQueryObject().
        Select("id", "name", "email").
        From("users").
        Where("active = 1").
        OrderBy("name", "ASC").
        Limit(10)

    fmt.Printf("Query SQL: %s\n", query.ToSQL())
}

Использована иллюстрация с сайта Мартина Фаулера.

Источник