274 lines
7.4 KiB
JavaScript
274 lines
7.4 KiB
JavaScript
// queryBuilder.js
|
|
class QueryBuilder {
|
|
constructor() {
|
|
this.conditions = []; // { sql, params }
|
|
this.limit = null;
|
|
this.offset = null;
|
|
this.groupBy = null;
|
|
this.orderBy = null;
|
|
}
|
|
|
|
where(condition, ...params) {
|
|
this.conditions.push({ sql: condition, params });
|
|
return this;
|
|
}
|
|
|
|
setLimit(limit) {
|
|
this.limit = limit;
|
|
return this;
|
|
}
|
|
|
|
setOffset(offset) {
|
|
this.offset = offset;
|
|
return this;
|
|
}
|
|
|
|
setGroupBy(groupBy) {
|
|
this.groupBy = groupBy;
|
|
return this;
|
|
}
|
|
|
|
orderByField(field, direction = 'ASC') {
|
|
this.orderBy = { field, direction: direction.toUpperCase() };
|
|
return this;
|
|
}
|
|
|
|
buildConditions() {
|
|
if (!this.conditions.length) return { sql: '', params: [] };
|
|
const sql = this.conditions.map(c => `(${c.sql})`).join(' AND ');
|
|
const params = this.conditions.flatMap(c => c.params);
|
|
return { sql, params };
|
|
}
|
|
|
|
buildLimitOffset() {
|
|
let sql = '';
|
|
if (this.limit !== null) sql += ` LIMIT ${this.limit}`;
|
|
if (this.offset !== null) sql += ` OFFSET ${this.offset}`;
|
|
return sql;
|
|
}
|
|
|
|
async execute(db) {
|
|
const { sql, params } = this.build();
|
|
const [rows] = await db.query(sql, params);
|
|
return rows;
|
|
}
|
|
}
|
|
|
|
// ------------------- SELECT -------------------
|
|
class SelectBuilder extends QueryBuilder {
|
|
constructor() {
|
|
super();
|
|
this.selectFields = [];
|
|
this.tables = [];
|
|
this.joins = [];
|
|
this.unions = [];
|
|
}
|
|
|
|
select(fields) {
|
|
if (typeof fields === 'string') {
|
|
this.selectFields = fields.split(',').map(f => f.trim());
|
|
} else if (Array.isArray(fields)) {
|
|
this.selectFields = fields;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
from(table) {
|
|
this.tables.push(table);
|
|
return this;
|
|
}
|
|
|
|
join(type, table, condition) {
|
|
this.joins.push(`${type.toUpperCase()} JOIN ${table} ON ${condition}`);
|
|
return this;
|
|
}
|
|
|
|
union(queryBuilder, type = 'UNION') {
|
|
this.unions.push({ queryBuilder, type });
|
|
return this;
|
|
}
|
|
|
|
unionAll(queryBuilder) {
|
|
return this.union(queryBuilder, 'UNION ALL');
|
|
}
|
|
|
|
build() {
|
|
const selectClause = this.selectFields.length ? this.selectFields.join(', ') : '*';
|
|
let sql = `SELECT ${selectClause} FROM ${this.tables.join(', ')}`;
|
|
if (this.joins.length) sql += ' ' + this.joins.join(' ');
|
|
|
|
const { sql: whereSql, params } = this.buildConditions();
|
|
if (whereSql) sql += ` WHERE ${whereSql}`;
|
|
|
|
if (this.groupBy) sql += ` GROUP BY ${this.groupBy}`;
|
|
|
|
// 处理 UNION
|
|
for (const u of this.unions) {
|
|
const { sql: uSql, params: uParams } = u.queryBuilder.build();
|
|
sql += ` ${u.type} ${uSql}`;
|
|
params.push(...uParams);
|
|
}
|
|
|
|
if (this.orderBy) {
|
|
sql += ` ORDER BY ${this.orderBy.field} ${this.orderBy.direction}`;
|
|
}
|
|
|
|
sql += this.buildLimitOffset();
|
|
return { sql, params };
|
|
}
|
|
|
|
async paginateWithCount(db, page = 1, pageSize = 10) {
|
|
if (page <= 0 || pageSize <= 0) {
|
|
throw new Error('分页参数必须大于0');
|
|
}
|
|
|
|
// count 查询
|
|
let countSql = `SELECT COUNT(*) as total FROM ${this.tables.join(', ')}`;
|
|
if (this.joins.length) countSql += ' ' + this.joins.join(' ');
|
|
|
|
const { sql: whereSql, params } = this.buildConditions();
|
|
if (whereSql) countSql += ` WHERE ${whereSql}`;
|
|
|
|
const [countRows] = await db.query(countSql, params);
|
|
const total = countRows[0].total;
|
|
|
|
// 数据查询
|
|
this.setLimit(pageSize).setOffset((page - 1) * pageSize);
|
|
const { sql, params: dataParams } = this.build();
|
|
const [rows] = await db.query(sql, dataParams);
|
|
|
|
return {
|
|
data: rows,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
totalPages: Math.ceil(total / pageSize)
|
|
};
|
|
}
|
|
}
|
|
|
|
// ------------------- UPDATE -------------------
|
|
class UpdateBuilder extends QueryBuilder {
|
|
constructor() {
|
|
super();
|
|
this.table = '';
|
|
this.updateFields = {};
|
|
}
|
|
|
|
update(table) {
|
|
this.table = table;
|
|
return this;
|
|
}
|
|
|
|
set(field, value) {
|
|
if (typeof value === 'object' && value.increment !== undefined) {
|
|
this.updateFields[field] = { increment: value.increment };
|
|
} else {
|
|
this.updateFields[field] = value;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
setFields(fieldsObj) {
|
|
Object.entries(fieldsObj).forEach(([k, v]) => this.set(k, v));
|
|
return this;
|
|
}
|
|
|
|
build() {
|
|
const updateClauses = Object.keys(this.updateFields).map(field => {
|
|
const val = this.updateFields[field];
|
|
if (typeof val === 'object' && val.increment !== undefined) {
|
|
return `${field} = ${field} + ?`;
|
|
}
|
|
return `${field} = ?`;
|
|
}).join(', ');
|
|
|
|
let sql = `UPDATE ${this.table} SET ${updateClauses}`;
|
|
const { sql: whereSql, params: whereParams } = this.buildConditions();
|
|
if (whereSql) sql += ` WHERE ${whereSql}`;
|
|
|
|
const params = [
|
|
...Object.values(this.updateFields).map(v =>
|
|
(typeof v === 'object' && v.increment !== undefined) ? v.increment : v
|
|
),
|
|
...whereParams
|
|
];
|
|
return { sql, params };
|
|
}
|
|
}
|
|
|
|
// ------------------- INSERT -------------------
|
|
class InsertBuilder extends QueryBuilder {
|
|
constructor() {
|
|
super();
|
|
this.table = '';
|
|
this.records = [];
|
|
this.updateValues = {};
|
|
}
|
|
|
|
into(table) {
|
|
this.table = table;
|
|
return this;
|
|
}
|
|
|
|
values(records) {
|
|
if (!Array.isArray(records)) records = [records];
|
|
this.records = records;
|
|
return this;
|
|
}
|
|
|
|
upsert(records, updateFields) {
|
|
this.values(records);
|
|
this.updateValues = updateFields;
|
|
return this;
|
|
}
|
|
|
|
build() {
|
|
if (!this.records.length) throw new Error('No values to insert');
|
|
|
|
const columns = Object.keys(this.records[0]);
|
|
const placeholders = `(${columns.map(() => '?').join(', ')})`;
|
|
const valuesClause = this.records.map(() => placeholders).join(', ');
|
|
const params = this.records.flatMap(r => columns.map(c => r[c]));
|
|
|
|
let sql = `INSERT INTO ${this.table} (${columns.join(', ')}) VALUES ${valuesClause}`;
|
|
|
|
if (Object.keys(this.updateValues).length) {
|
|
const updates = Object.keys(this.updateValues)
|
|
.map(f => `${f} = VALUES(${f})`) // MySQL 8.0+ 兼容写法
|
|
.join(', ');
|
|
sql += ` ON DUPLICATE KEY UPDATE ${updates}`;
|
|
}
|
|
|
|
return { sql, params };
|
|
}
|
|
}
|
|
|
|
// ------------------- DELETE -------------------
|
|
class DeleteBuilder extends QueryBuilder {
|
|
constructor() {
|
|
super();
|
|
this.table = '';
|
|
}
|
|
|
|
from(table) {
|
|
this.table = table;
|
|
return this;
|
|
}
|
|
|
|
build() {
|
|
let sql = `DELETE FROM ${this.table}`;
|
|
const { sql: whereSql, params } = this.buildConditions();
|
|
if (!whereSql) throw new Error('DELETE without WHERE is not allowed!');
|
|
sql += ` WHERE ${whereSql}`;
|
|
return { sql, params };
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
SelectBuilder,
|
|
UpdateBuilder,
|
|
InsertBuilder,
|
|
DeleteBuilder
|
|
};
|