node-postgres: як виконати запит “WHERE col IN (<динамічний список значень>)”?


81

Я намагаюся виконати такий запит:

SELECT * FROM table WHERE id IN (1,2,3,4)

Проблема полягає в тому, що список ідентифікаторів, за якими я хочу фільтруватись, не є постійним і повинен бути різним при кожному виконанні. Мені також потрібно було б уникати ідентифікаторів, оскільки вони можуть надходити з ненадійних джерел, хоча я б насправді уникав усього, що входить у запит, незалежно від надійності джерела.

node-postgres, схоже, працює виключно з прив'язаними параметрами client.query('SELECT * FROM table WHERE id = $1', [ id ]):; це спрацює, якщо б у мене була відома кількість значень ( client.query('SELECT * FROM table WHERE id IN ($1, $2, $3)', [ id1, id2, id3 ])), але безпосередньо з масивом працювати не буде:, client.query('SELECT * FROM table WHERE id IN ($1)', [ arrayOfIds ])оскільки, здається, не існує жодної спеціальної обробки параметрів масиву.

Побудова шаблону запиту динамічно відповідно до кількості елементів у масиві та розширення масиву ідентифікаторів у масив параметрів запиту (який у моєму фактичному випадку також містить інші параметри, крім списку ідентифікаторів) здається необґрунтовано обтяжливим. Жорстке кодування списку ідентифікаторів у шаблоні запиту також здається нежиттєздатним, оскільки node-postgres не надає жодних методів екранування значень.

Це здається дуже поширеним випадком використання, тому я припускаю, що я насправді щось пропускаю, а не те, що неможливо використовувати загальний IN (values)оператор SQL із node-postgres.

Якщо хтось вирішив цю проблему більш елегантно, ніж ті, що я перелічив вище, або якщо мені дійсно щось не вистачає щодо node-postgres, будь ласка, допоможіть.


Я не пробував цього сам, але, схоже, ви хочете передати вкладений масив як перший (і в цьому випадку єдиний) елемент вашого масиву підстановок, оскільки він очікує, що кожен елемент у цьому параметрі буде значенням заміщення. Приклад: client.query ('SELECT * FROM table WHERE id IN ($ 1)', [[arrayOfIds]]);
Ryan LaB,

Ні, не працює так (передбачувано). Очевидно, він намагається представити масив [1, 2, 3] як значення рядка "1,2,3", а сервер повертає помилку "невірний синтаксис введення для цілого числа".
lanzz

Чи можете ви опублікувати повний запит, який він намагається виконати, передаючи масив таким чином? Я не такий знайомий з Posgres, як MySQL, але чи не так ви представляєте цей запит? IN (1,2,3) мені здається правильним. Якщо я повністю не зрозумів, що ти намагаєшся зробити.
Райан ЛаБ

Я не бачу жодного способу отримати повний запит, оскільки фактичне повідомлення мережевого протоколу передає шаблон запиту та список параметрів окремо на сервер postgres, а остаточна композиція відбувається на стороні сервера. Навіть у журналі postgres немає повного запиту: 2012-05-26 20:31:08 EEST ERROR: invalid input syntax for integer: "1,3" 2012-05-26 20:31:08 EEST STATEMENT: SELECT * FROM users WHERE id IN ($1)
lanzz

Відповіді:


52

Ми вже бачили це питання в списку питань github. Правильний спосіб - це динамічно генерувати ваш список параметрів на основі масиву. Щось на зразок цього:

var arr = [1, 2, "hello"];
var params = [];
for(var i = 1; i <= arr.length; i++) {
  params.push('$' + i);
}
var queryText = 'SELECT id FROM my_table WHERE something IN (' + params.join(',') + ')';
client.query(queryText, arr, function(err, cb) {
 ...
});

Таким чином ви отримуєте параметризований екран екранування postgres.


9
Оскільки ми знаходимося в Node.js, ви можете безпечно використовувати рідну карту (). Це ще більше спрощує код: params = arr.map (function (item, idx) {return '$' + idx});
srigi

8
Збирався закричати криваве вбивство, ввівши відкритий текст у запит! Виявляється, це просто доларові символи та цифри -_-'....
Лодевейк

7
Пропозиція @srigi має помилку. Це має бути: var params = arr.map (function (item, idx) {return '$' + (idx + 1);});
Гептик

32
Оновлення цієї відповіді тепер є у поширених запитаннях щодо node-postgres. Наступні роботи: client.query("SELECT * FROM stooges WHERE name = ANY ($1)", [ ['larry', 'curly', 'moe'] ], ...);Дивіться тут: github.com/brianc/node-postgres/wiki/…
Siegel

2
@SandeepSinghRana Я не бачу, наскільки це вразливо до ін'єкції SQL. Створений рядок запиту гарантовано має таку форму, SELECT id ... IN ($1, $2, ... $N)яка не допускає можливості введення - це лише параметризований $запит, готовий до заповнення. (Ми ніколи не додаємо фактичні значення запиту до рядка!) Тоді популяція фактичного значення відбувається через бібліотеку, яка за своєю природою не захищена від введення SQL (якщо в бібліотеці немає серйозної помилки).
apsillers

109

Схоже , що ви , можливо, були близькі засновані на ваш коментар до @ ebohlman в відповідь . Можна використовувати WHERE id = ANY($1::int[]). PostgreSQL перетворить масив на тип, до якого додано параметр $1::int[]. Отже, ось надуманий приклад, який працює для мене:

var ids = [1,3,4]; 

var q = client.query('SELECT Id FROM MyTable WHERE Id = ANY($1::int[])',[ids]);

q.on('row', function(row) {
  console.log(row);
})

// outputs: { id: 1 }
//          { id: 3 }
//          { id: 4 }

Тут відсутнє будь-яке цитування значень у arrта node-postgresне передбачає жодних методів цитування. Я шукаю "правильний" спосіб вирішити цю проблему, тому я не хочу впроваджувати власний код літерального цитування SQL. Крім того, якщо я піду в цьому напрямку, я скоріше вбудую список ідентифікаторів безпосередньо в шаблон запиту, замість того, щоб готувати літерал масиву лише для того, щоб його знову проаналізувати на стороні сервера.
lanzz

Чи можете ви розширити те, що ви маєте на увазі під "будь-яким цитуванням значень у arr", будь ласка?
Перо П.

Це означає, що якщо arrкод не містить цілих чисел, але, скажімо, рядків, які містять коми або дужки, ваш код не спрацює або буде працювати неправильно.
lanzz

2
Звичайно, так, це лише приклад. Звичайно, ви дезінфікували INпараметри цього пункту перед тим, як готувати заяву, незалежно від реалізації. Можливо, я неправильно зрозумів підстави вашого запитання.
Перо П.

6
Отже, враховуючи цю відповідь, використовується параметризований запит, і тому параметри аналізуються та захищаються на сервері postgres, де ризик безпеки? Якщо він є, то є більші проблеми.
Pero P.

26

Найкращим рішенням, яке я знайшов, було використання ANYфункції з примусом масиву Postgres. Це дозволяє зіставляти стовпець із довільним масивом значень, як ніби виписали col IN (v1, v2, v3). Це підхід у відповіді pero, але тут я показую, що продуктивність ANYтакої ж, як IN.

Запит

Ваш запит повинен виглядати так:

SELECT * FROM table WHERE id = ANY($1::int[])

Цей біт у кінці, де написано, $1::int[]можна змінити відповідно до типу вашого стовпця "id". Наприклад, якщо тип ваших ідентифікаторів - uuidви б написали, $1::uuid[]щоб примусити аргумент до масиву UUID. Перегляньте тут список типів даних Postgres .

Це простіше, ніж написання коду для побудови рядка запиту, і безпечно від ін’єкцій SQL.

Приклад

З node-postgres повний приклад JavaScript виглядає так:

var pg = require('pg');

var client = new pg.Client('postgres://username:password@localhost/database');
client.connect(function(err) {
  if (err) {
    throw err;
  }

  var ids = [23, 65, 73, 99, 102];
  client.query(
    'SELECT * FROM table WHERE id = ANY($1::int[])',
    [ids],  // array of query arguments
    function(err, result) {
      console.log(result.rows);
    }
  );
});

Продуктивність

Одним з найкращих способів зрозуміти ефективність запиту SQL - це подивитися, як його обробляє база даних. Зразкова таблиця має близько 400 рядків і первинний ключ типу "id" text.

EXPLAIN SELECT * FROM tests WHERE id = ANY('{"test-a", "test-b"}');
EXPLAIN SELECT * FROM tests WHERE id IN ('test-a', 'test-b');

В обох випадках Postgres повідомив той самий план запитів:

Bitmap Heap Scan on tests  (cost=8.56..14.03 rows=2 width=79)
  Recheck Cond: (id = ANY ('{test-a,test-b}'::text[]))
  ->  Bitmap Index Scan on tests_pkey  (cost=0.00..8.56 rows=2 width=0)
        Index Cond: (id = ANY ('{test-a,test-b}'::text[]))

Ви можете бачити інший план запиту, залежно від розміру вашої таблиці, де є індекс, та вашого запиту. Але для таких запитів, як наведені вище, ANYі INобробляються однаково.


Зауважте, що хоча це справедливо для форми БУДЬ-ЯКОГО набору, існує друга форма для кожного IN () та = БУДЬ-ЯКОГО (), і вони не є повністю еквівалентними. Розглянемо: stackoverflow.com/questions/34627026/…
Jonas Kello

17

Використовуючи pg-promis , це добре працює через CSV-фільтр (значення, розділені комами):

const values = [1, 2, 3, 4];

db.any('SELECT * FROM table WHERE id IN ($1:csv)', [values])
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log(error);
    });

А щоб вирішити стурбованість різними типами даних, :csvмодифікатор серіалізує масив у формат CSV, одночасно перетворюючи всі значення у належний формат PostgreSQL відповідно до їх типу JavaScript, навіть підтримуючи спеціальне форматування типу .

І якщо у вас є значення змішаного типу, як це:, const values = [1, 'two', null, true]ви все одно отримаєте правильно екранований SQL:

SELECT * FROM table WHERE id IN (1, 'two', null, true)

ОНОВЛЕННЯ

З версії 7.5.5 pg- promis почав підтримувати :listяк взаємозамінний псевдонім для :csvфільтра:

db.any('SELECT * FROM table WHERE id IN ($1:list)', [values])

1
Полюбіть цю бібліотеку
Квінтін Ботес,

0

Іншим можливим рішенням є використання такої UNNESTфункції:

 var ids = [23, 65, 73, 99, 102];
 var strs = ['bar', 'tar', 'far']
 client.query(
   'SELECT * FROM table WHERE id IN(SELECT(UNNEST($1))',
    [ids],  // array of query arguments
    function(err, result) {
       console.log(result.rows);
    }
);
client.query(
   'SELECT * FROM table WHERE id IN(SELECT(UNNEST($1))',
    [strs],  // array of query arguments
    function(err, result) {
       console.log(result.rows);
    }
);

Я використовував це в збереженій процедурі, і вона працює нормально. Вважайте, що це також має працювати з коду node-pg.

Про функцію UNNEST ви можете прочитати тут .


1
Це здається величезним надлишком, порівняно з id = ANY($1)рішеннями
lanzz

0

Іншим можливим рішенням є, наприклад, REST API у NODE JS:

var name = req.body;//Body is a objetc that has properties for example provinces
var databaseRB = "DATABASENAME"
var conStringRB = "postgres://"+username+":"+password+"@"+host+"/"+databaseRB; 

var filter_query = "SELECT row_to_json(fc) FROM ( SELECT 'FeatureCollection' As type, array_to_json(array_agg(f)) As features FROM (SELECT 'Feature' As type, ST_AsGeoJSON(lg.geom)::json As geometry, row_to_json((parameters) As properties FROM radiobases As lg WHERE lg.parameter= ANY($1) )As f) As fc";

var client = new pg.Client(conStringRB);
client.connect();
var query = client.query(new Query(filter_query,[name.provinces]));
query.on("row", function (row, result) {
  result.addRow(row);
});
query.on("end", function (result) {
 var data = result.rows[0].row_to_json
   res.json({
     title: "Express API",
     jsonData: data
     });
});

Майте на увазі, що можна використовувати будь-який тип масиву


-1

Ідея загалом:

var invals = [1,2,3,4], cols = [...fields];
var setvs = vs => vs.map(v=> '$'+ (values.push(v))  ).join();

var values = [];
var text = 'SELECT '+ setvs(cols) +' FROM table WHERE id IN (' + setvs(invals) +')';
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.