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())
}
Использована иллюстрация с сайта Мартина Фаулера.