Интерфейс PostgreSQL для Node.js
Эта библиотека, построенная на основе node-postgres, добавляет следующее:
На момент своего создания в 2015 году эта библиотека только добавляла промисы к базовому драйверу, отсюда и название pg-promise
. И хотя оригинальное название было сохранено, функциональность библиотеки была значительно расширена, и обещания теперь составляют лишь малую ее часть.
Я оказываю бесплатную поддержку здесь и на StackOverflow.
И если вы хотите помочь этому проекту, я могу принять биткойны: 1yki7MXMkuDw8qqe5icVdh1GJZSQSzKZp
В главе «Использование» ниже объясняются основы, которые вам необходимо знать, а официальная документация поможет вам начать работу и содержит ссылки на все другие ресурсы.
Пожалуйста, прочтите Примечания к вкладу, прежде чем открывать какой-либо новый выпуск или PR.
После создания объекта базы данных в соответствии с шагами, описанными в официальной документации, вы получаете доступ к методам, описанным ниже.
Все методы запроса библиотеки основаны на запросе универсального метода.
Обычно для выполнения запросов следует использовать только производные, специфичные для результата методы, все из которых названы в соответствии с тем, сколько строк данных ожидается вернуть запросом, поэтому для каждого запроса вы должны выбрать правильный метод: нет, один, oneOrNone, многие, многиеOrNone = любой. Не путайте имя метода с количеством строк, на которые будет воздействовать запрос, это совершенно не имеет значения.
Полагаясь на методы, ориентированные на результат, вы защищаете свой код от неожиданного количества строк данных, которые будут автоматически отклонены (расценены как ошибки).
Есть также несколько конкретных методов, которые вам часто понадобятся:
Протокол полностью настраиваемый/расширяемый посредством расширения событий.
ВАЖНЫЙ:
Наиболее важные методы, которые нужно понять с самого начала, — это Task и tx/txIf (см. Задачи и Транзакции). Как описано для запроса метода, он получает и освобождает соединение, что делает его плохим выбором для одновременного выполнения нескольких запросов. По этой причине «Связывание запросов» является обязательным к прочтению, чтобы избежать написания кода, который неправильно использует соединения.
«Обучение на примере» — это руководство для начинающих, основанное на примерах.
Эта библиотека поставляется со встроенным механизмом форматирования запросов, который обеспечивает высокопроизводительное экранирование значений, гибкость и расширяемость. Он используется по умолчанию со всеми методами запроса, если вы полностью не откажетесь от него с помощью параметра pgFormatting
в параметрах инициализации.
Все методы форматирования, используемые внутри, доступны из пространства имен форматирования, поэтому при необходимости их также можно использовать напрямую. Основным методом является формат, используемый каждым методом запроса для форматирования запроса.
Синтаксис форматирования переменных определяется типом передаваемых values
:
values
представляют собой массив или один базовый тип;values
являются объектами (кроме Array
или null
).ВНИМАНИЕ: Никогда не используйте строки шаблона ES6 или ручную конкатенацию для генерации запросов, поскольку и то, и другое может легко привести к поломке запросов! Только механизм форматирования этой библиотеки знает, как правильно экранировать значения переменных для PostgreSQL.
Простейшее (классическое) форматирование использует синтаксис $1, $2, ...
для ввода значений в строку запроса на основе их индекса (от $1
до $100000
) из массива значений:
await db . any ( 'SELECT * FROM product WHERE price BETWEEN $1 AND $2' , [ 1 , 10 ] )
Механизм форматирования также поддерживает параметризацию с одним значением для запросов, которые используют только переменную $1
:
await db . any ( 'SELECT * FROM users WHERE name = $1' , 'John' )
Однако это работает только для типов number
, bigint
, string
, boolean
, Date
и null
, поскольку такие типы, как Array
и Object
меняют способ интерпретации параметров. Вот почему передача индексных переменных внутри массива рекомендуется как более безопасная, чтобы избежать двусмысленности.
Когда метод запроса параметризуется values
в качестве объекта, механизм форматирования ожидает, что запрос будет использовать синтаксис именованного параметра $*propName*
, где *
— любая из следующих пар открытия-закрытия: {}
, ()
, <>
, []
, //
.
// We can use every supported variable syntax at the same time, if needed:
await db . none ( 'INSERT INTO users(first_name, last_name, age) VALUES(${name.first}, $, $/age/)' , {
name : { first : 'John' , last : 'Dow' } ,
age : 30
} ) ;
ВАЖНО: Никогда не используйте зарезервированный синтаксис ${}
внутри строк шаблона ES6, поскольку они не знают, как форматировать значения для PostgreSQL. Внутри строк шаблона ES6 вы должны использовать только одну из 4 альтернатив — $()
, $<>
, $[]
или $//
. В общем, вам следует либо использовать стандартные строки для SQL, либо помещать SQL во внешние файлы — см. Файлы запросов.
Допустимые имена переменных ограничены синтаксисом переменных JavaScript с открытыми именами. И имя this
имеет особое значение — оно относится к самому объекту форматирования (см. ниже).
Имейте в виду, что хотя значения свойств null
и undefined
отформатированы как null
, если свойство не существует, выдается ошибка.
this
ссылка
Свойство this
относится к самому объекту форматирования, который должен быть вставлен в виде строки в формате JSON.
await db . none ( 'INSERT INTO documents(id, doc) VALUES(${id}, ${this})' , {
id : 123 ,
body : 'some text'
} )
//=> INSERT INTO documents(id, doc) VALUES(123, '{"id":123,"body":"some text"}')
Именованные параметры поддерживают вложенность имен свойств любой глубины.
const obj = {
one : {
two : {
three : {
value1 : 123 ,
value2 : a => {
// a = obj.one.two.three
return 'hello' ;
} ,
value3 : function ( a ) {
// a = this = obj.one.two.three
return 'world' ;
} ,
value4 : {
toPostgres : a => {
// Custom Type Formatting
// a = obj.one.two.three.value4
return a . text ;
} ,
text : 'custom'
}
}
}
}
} ;
await db . one ( 'SELECT ${one.two.three.value1}' , obj ) ; //=> SELECT 123
await db . one ( 'SELECT ${one.two.three.value2}' , obj ) ; //=> SELECT 'hello'
await db . one ( 'SELECT ${one.two.three.value3}' , obj ) ; //=> SELECT 'world'
await db . one ( 'SELECT ${one.two.three.value4}' , obj ) ; //=> SELECT 'custom'
Фамилия в разрешении может быть любой, в том числе:
т.е. цепочка разрешения бесконечно гибка и поддерживает рекурсию без ограничений.
Однако обратите внимание, что вложенные параметры не поддерживаются в пространстве имен помощников.
По умолчанию все значения форматируются в соответствии с их типом JavaScript. Фильтры форматирования (или модификаторы) измените это, чтобы значение форматировалось по-другому.
Обратите внимание, что фильтры форматирования работают только для обычных запросов и недоступны в ReadedStatement или ParameterizedQuery, поскольку они по определению форматируются на стороне сервера.
Фильтры используют один и тот же синтаксис для индексных переменных и именованных параметров, сразу после имени переменной:
await db . any ( 'SELECT $1:name FROM $2:name' , [ 'price' , 'products' ] )
//=> SELECT "price" FROM "products"
await db . any ( 'SELECT ${column:name} FROM ${table:name}' , {
column : 'price' ,
table : 'products'
} ) ;
//=> SELECT "price" FROM "products"
Поддерживаются следующие фильтры:
:name
/ ~
— SQL-имена:alias
- Фильтр псевдонимов:raw
/ ^
— необработанный текст:value
/ #
— Открыть значения:csv
/ :list
— CSV-фильтр:json
— Фильтр JSON Когда имя переменной заканчивается на :name
или более короткий синтаксис ~
(тильда), оно представляет собой имя или идентификатор SQL, которые необходимо соответствующим образом экранировать:
await db . query ( 'INSERT INTO $1~($2~) VALUES(...)' , [ 'Table Name' , 'Column Name' ] ) ;
//=> INSERT INTO "Table Name"("Column Name") VALUES(...)
await db . query ( 'INSERT INTO $1:name($2:name) VALUES(...)' , [ 'Table Name' , 'Column Name' ] ) ;
//=> INSERT INTO "Table Name"("Column Name") VALUES(...)
Обычно переменная имени SQL представляет собой текстовую строку длиной не менее 1 символа. Однако pg-promise
поддерживает различные способы предоставления имен SQL:
*
(звездочки), автоматически распознается как все столбцы : await db . query ( 'SELECT $1:name FROM $2:name' , [ '*' , 'table' ] ) ;
//=> SELECT * FROM "table"
await db . query ( 'SELECT ${columns:name} FROM ${table:name}' , {
columns : [ 'column1' , 'column2' ] ,
table : 'table'
} ) ;
//=> SELECT "column1","column2" FROM "table"
const obj = {
one : 1 ,
two : 2
} ;
await db . query ( 'SELECT $1:name FROM $2:name' , [ obj , 'table' ] ) ;
//=> SELECT "one","two" FROM "table"
Кроме того, синтаксис this
перечисление имен столбцов из объекта форматирования:
const obj = {
one : 1 ,
two : 2
} ;
await db . query ( 'INSERT INTO table(${this:name}) VALUES(${this:csv})' , obj ) ;
//=> INSERT INTO table("one","two") VALUES(1, 2)
Использование этого типа форматирования имен и идентификаторов SQL вместе с обычным форматированием переменных защищает ваше приложение от SQL-инъекций.
Метод as.name реализует форматирование.
Псевдоним — это более простая и менее строгая версия фильтра :name
, которая поддерживает только текстовую строку, т. е. не поддерживает *
, this
, массив или объект в качестве входных данных, как это делает :name
. Однако он поддерживает и другие популярные случаи, которые менее строги, но охватывают не менее 99% всех вариантов использования, как показано ниже.
await db . any ( 'SELECT full_name as $1:alias FROM $2:name' , [ 'name' , 'table' ] ) ;
//=> SELECT full_name as name FROM "table"
.
, а затем экранировать каждую часть отдельно, поддерживая тем самым автоматические составные имена SQL: await db . any ( 'SELECT * FROM $1:alias' , [ 'schemaName.table' ] ) ;
//=> SELECT * FROM "schemaName".table
Более подробную информацию см. в методе as.alias, реализующем форматирование.
Если имя переменной заканчивается на :raw
или более короткий синтаксис ^
, значение должно быть введено как необработанный текст без экранирования.
Такие переменные не могут быть null
или undefined
из-за неоднозначного значения в этом случае, и эти значения будут выдавать ошибку Values null/undefined cannot be used as raw text.
const where = pgp . as . format ( 'WHERE price BETWEEN $1 AND $2' , [ 5 , 10 ] ) ; // pre-format WHERE condition
await db . any ( 'SELECT * FROM products $1:raw' , where ) ;
//=> SELECT * FROM products WHERE price BETWEEN 5 AND 10
Поддерживается специальный синтаксис this:raw
/ this^
для внедрения объекта форматирования в виде необработанной строки JSON.
ПРЕДУПРЕЖДЕНИЕ:
Этот фильтр небезопасен, и его не следует использовать для значений, поступающих со стороны клиента, поскольку это может привести к внедрению SQL.
Когда имя переменной заканчивается на :value
или более короткий синтаксис #
, оно экранируется как обычно, за исключением случаев, когда ее тип является строкой, конечные кавычки не добавляются.
Открытые значения в первую очередь предназначены для того, чтобы иметь возможность составлять полные динамические операторы LIKE
/ ILIKE
во внешних файлах SQL без необходимости генерировать их в коде.
т.е. вы можете либо создать такой фильтр в своем коде:
const name = 'John' ;
const filter = '%' + name + '%' ;
а затем передать ее как обычную строковую переменную или вы можете передать только name
и ваш запрос будет использовать синтаксис открытого значения для добавления дополнительной логики поиска:
SELECT * FROM table WHERE name LIKE ' %$1:value% ' )
ПРЕДУПРЕЖДЕНИЕ:
Этот фильтр небезопасен, и его не следует использовать для значений, поступающих со стороны клиента, поскольку это может привести к внедрению SQL.
Метод as.value реализует форматирование.
Если имя переменной заканчивается на :json
, к значению применяется явное форматирование JSON.
По умолчанию любой объект, который не является Date
, Array
, Buffer
, null
или Custom-Type (см. Форматирование пользовательского типа), автоматически форматируется как JSON.
Метод as.json реализует форматирование.
Когда имя переменной заканчивается на :csv
или :list
, оно форматируется как список значений, разделенных запятыми, причем каждое значение форматируется в соответствии с его типом JavaScript.
Обычно вы используете это для значения, являющегося массивом, хотя это работает и для одиночных значений. См. примеры ниже.
const ids = [ 1 , 2 , 3 ] ;
await db . any ( 'SELECT * FROM table WHERE id IN ($1:csv)' , [ ids ] )
//=> SELECT * FROM table WHERE id IN (1,2,3)
const ids = [ 1 , 2 , 3 ] ;
await db . any ( 'SELECT * FROM table WHERE id IN ($1:list)' , [ ids ] )
//=> SELECT * FROM table WHERE id IN (1,2,3)
Использование автоматического перечисления свойств:
const obj = { first : 123 , second : 'text' } ;
await db . none ( 'INSERT INTO table($1:name) VALUES($1:csv)' , [ obj ] )
//=> INSERT INTO table("first","second") VALUES(123,'text')
await db . none ( 'INSERT INTO table(${this:name}) VALUES(${this:csv})' , obj )
//=> INSERT INTO table("first","second") VALUES(123,'text')
const obj = { first : 123 , second : 'text' } ;
await db . none ( 'INSERT INTO table($1:name) VALUES($1:list)' , [ obj ] )
//=> INSERT INTO table("first","second") VALUES(123,'text')
await db . none ( 'INSERT INTO table(${this:name}) VALUES(${this:list})' , obj )
//=> INSERT INTO table("first","second") VALUES(123,'text')
Метод as.csv реализует форматирование.
Библиотека поддерживает двойной синтаксис для CTF (форматирование пользовательского типа):
Библиотека всегда сначала проверяет символический CTF, и если такой синтаксис не используется, только затем проверяется явный CTF.
Любое значение/объект, реализующий функцию toPostgres
рассматривается как тип пользовательского форматирования. Затем вызывается функция, чтобы получить фактическое значение, передав ему объект через this
контекст и плюс в качестве одного параметра (в случае, если toPostgres
является стрелочной функцией ES6):
const obj = {
toPostgres ( self ) {
// self = this = obj
// return a value that needs proper escaping
}
}
Функция toPostgres
может возвращать что угодно, в том числе другой объект со своей функцией toPostgres
, т.е. поддерживаются вложенные пользовательские типы.
Значение, возвращаемое из toPostgres
, экранируется в соответствии с его типом JavaScript, за исключением случаев, когда объект также содержит свойство rawType
, для которого установлено истинное значение. В этом случае возвращаемое значение считается предварительно отформатированным и, таким образом, внедряется напрямую как необработанный текст:
const obj = {
toPostgres ( self ) {
// self = this = obj
// return a pre-formatted value that does not need escaping
} ,
rawType : true // use result from toPostgres directly, as Raw Text
}
В приведенном ниже примере реализуется класс, который автоматически форматирует ST_MakePoint
по координатам:
class STPoint {
constructor ( x , y ) {
this . x = x ;
this . y = y ;
this . rawType = true ; // no escaping, because we return pre-formatted SQL
}
toPostgres ( self ) {
return pgp . as . format ( 'ST_MakePoint($1, $2)' , [ this . x , this . y ] ) ;
}
}
А классический синтаксис такого класса еще проще:
function STPoint ( x , y ) {
this . rawType = true ; // no escaping, because we return pre-formatted SQL
this . toPostgres = ( ) => pgp . as . format ( 'ST_MakePoint($1, $2)' , [ x , y ] ) ;
}
С помощью этого класса вы можете использовать new STPoint(12, 34)
в качестве значения форматирования, которое будет правильно введено.
Вы также можете использовать CTF для переопределения любого стандартного типа:
Date . prototype . toPostgres = a => a . getTime ( ) ;
Единственное отличие от Explicit CTF заключается в том, что мы установили toPostgres
и rawType
как свойства символа ES6, определенные в пространстве имен ctf:
const { toPostgres , rawType } = pgp . as . ctf ; // Global CTF symbols
const obj = {
[ toPostgres ] ( self ) {
// self = this = obj
// return a pre-formatted value that does not need escaping
} ,
[ rawType ] : true // use result from toPostgres directly, as Raw Text
} ;
Поскольку символы CTF являются глобальными, вы также можете настраивать объекты независимо от этой библиотеки:
const ctf = {
toPostgres : Symbol . for ( 'ctf.toPostgres' ) ,
rawType : Symbol . for ( 'ctf.rawType' )
} ;
В остальном он работает точно так же, как Explicit CTF, но без изменения подписи объекта.
Если вы не знаете, что это значит, прочтите API символов ES6 и его использование для уникальных имен свойств. Короче говоря, свойства символа не перечисляются через for(name in obj)
, т.е. они обычно не видны в JavaScript, а только через специальный API Object.getOwnPropertySymbols
.
Использование внешних файлов SQL (через QueryFile) дает множество преимуществ:
debug
), без перезапуска приложения;params
), автоматизация двухэтапного форматирования SQL;minify
+ compress
) для раннего обнаружения ошибок и уплотнения запросов. const { join : joinPath } = require ( 'path' ) ;
// Helper for linking to external query files:
function sql ( file ) {
const fullPath = joinPath ( __dirname , file ) ;
return new pgp . QueryFile ( fullPath , { minify : true } ) ;
}
// Create a QueryFile globally, once per file:
const sqlFindUser = sql ( './sql/findUser.sql' ) ;
db . one ( sqlFindUser , { id : 123 } )
. then ( user => {
console . log ( user ) ;
} )
. catch ( error => {
if ( error instanceof pgp . errors . QueryFileError ) {
// => the error is related to our QueryFile
}
} ) ;
Файл findUser.sql
:
/*
multi-line comments are supported
*/
SELECT name, dob -- single-line comments are supported
FROM Users
WHERE id = ${id}
Каждый метод запроса библиотеки может принимать тип QueryFile в качестве параметра query
. Тип QueryFile никогда не выдает никаких ошибок, оставляя методы запроса корректно отклонять их с помощью QueryFileError.
Использование именованных параметров во внешних файлах SQL рекомендуется вместо индексных переменных, поскольку это значительно упрощает чтение и понимание SQL, а также поскольку оно также допускает вложенные именованные параметры, поэтому переменные в большом и сложном файле SQL можно группировать в пространствах имен. для еще более легкого визуального разделения.
Задача представляет собой общее соединение для выполнения нескольких запросов:
db . task ( t => {
// execute a chain of queries against the task context, and return the result:
return t . one ( 'SELECT count(*) FROM events WHERE id = $1' , 123 , a => + a . count )
. then ( count => {
if ( count > 0 ) {
return t . any ( 'SELECT * FROM log WHERE event_id = $1' , 123 )
. then ( logs => {
return { count , logs } ;
} )
}
return { count } ;
} ) ;
} )
. then ( data => {
// success, data = either {count} or {count, logs}
} )
. catch ( error => {
// failed
} ) ;
Задачи предоставляют общий контекст соединения для своей функции обратного вызова, который должен быть освобожден после завершения, и их необходимо использовать при выполнении более одного запроса одновременно. См. также «Связывание запросов», чтобы понять важность использования задач.
При желании вы можете пометить задачи (см. Теги) и использовать асинхронный синтаксис ES7:
db . task ( async t => {
const count = await t . one ( 'SELECT count(*) FROM events WHERE id = $1' , 123 , a => + a . count ) ;
if ( count > 0 ) {
const logs = await t . any ( 'SELECT * FROM log WHERE event_id = $1' , 123 ) ;
return { count