واجهة PostgreSQL لـ Node.js
بنيت هذه المكتبة على رأس العقدة postgres، وتضيف ما يلي:
عند إنشائها في عام 2015، كانت هذه المكتبة تضيف الوعود فقط إلى برنامج التشغيل الأساسي، ومن هنا جاء اسم pg-promise
. وبينما تم الاحتفاظ بالاسم الأصلي، تم توسيع وظائف المكتبة إلى حد كبير، حيث أصبحت الوعود الآن مجرد جزء صغير منها.
أقدم دعمًا مجانيًا هنا وعلى StackOverflow.
وإذا كنت تريد المساعدة في هذا المشروع، فيمكنني قبول عملة البيتكوين: 1yki7MXMkuDw8qqe5icVdh1GJZSQSzKZp
يشرح فصل الاستخدام أدناه الأساسيات التي تحتاج إلى معرفتها، بينما تساعدك الوثائق الرسمية على البدء، وتوفر روابط لجميع الموارد الأخرى.
يرجى قراءة ملاحظات المساهمة قبل فتح أي عدد جديد أو علاقات عامة.
بمجرد إنشاء كائن قاعدة البيانات، وفقًا للخطوات الواردة في الوثائق الرسمية، يمكنك الوصول إلى الطرق الموثقة أدناه.
تعتمد جميع طرق الاستعلام الخاصة بالمكتبة على استعلام الطريقة العامة.
يجب عليك عادةً استخدام الأساليب المشتقة الخاصة بالنتائج فقط لتنفيذ الاستعلامات، والتي يتم تسميتها جميعًا وفقًا لعدد صفوف البيانات التي من المتوقع أن يعرضها الاستعلام، لذلك يجب عليك اختيار الطريقة الصحيحة لكل استعلام: لا شيء، واحد، oneOrNone، كثير، ManyOrNone = أي. لا تخلط بين اسم الطريقة وعدد الصفوف التي ستتأثر بالاستعلام، فهو غير ذي صلة على الإطلاق.
من خلال الاعتماد على الأساليب الخاصة بالنتائج، يمكنك حماية التعليمات البرمجية الخاصة بك من عدد غير متوقع من صفوف البيانات، التي سيتم رفضها تلقائيًا (يتم التعامل معها على أنها أخطاء).
هناك أيضًا بعض الطرق المحددة التي ستحتاج إليها غالبًا:
البروتوكول قابل للتخصيص/التمديد بالكامل عبر تمديد الحدث.
مهم:
أهم الطرق التي يجب فهمها من البداية هي المهمة وtx/txIf (راجع المهام والمعاملات). كما هو موثق بالنسبة للاستعلام عن الطريقة، فإنه يكتسب الاتصال ويحرره، مما يجعله خيارًا سيئًا لتنفيذ استعلامات متعددة في وقت واحد. لهذا السبب، يجب قراءة Chaining Queries لتجنب كتابة التعليمات البرمجية التي تسيء استخدام الاتصالات.
التعلم بالقدوة هو برنامج تعليمي للمبتدئين يعتمد على الأمثلة.
تأتي هذه المكتبة مع محرك تنسيق الاستعلام المضمن الذي يوفر هروبًا عالي الأداء ومرونة وقابلية للتوسعة. يتم استخدامه بشكل افتراضي مع جميع طرق الاستعلام، إلا إذا قمت بإلغاء الاشتراك فيه بالكامل عبر الخيار 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، يجب عليك فقط استخدام أحد البدائل الأربعة - $()
أو $<>
أو $[]
أو $//
. بشكل عام، يجب عليك إما استخدام السلاسل القياسية لـ 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 الخاص بها. تقوم عوامل تصفية التنسيق (أو المعدلات) بتغيير ذلك، بحيث يتم تنسيق القيمة بشكل مختلف.
لاحظ أن عوامل تصفية التنسيق تعمل فقط مع الاستعلامات العادية، ولا تكون متوفرة ضمن PreparationStatement أو 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 عبارة عن سلسلة نصية، والتي يجب أن تتكون من حرف واحد على الأقل. ومع ذلك، يدعم 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، ولكن دون تغيير توقيع الكائن.
إذا كنت لا تعرف ما يعنيه ذلك، فاقرأ واجهة برمجة تطبيقات رمز ES6 واستخدامها لأسماء الخصائص الفريدة. لكن باختصار، لا يتم تعداد خصائص الرمز عبر for(name in obj)
، أي أنها ليست مرئية بشكل عام داخل JavaScript، فقط من خلال 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