Tên bảng dưới dạng tham số hàm PostgreSQL


85

Tôi muốn chuyển tên bảng làm tham số trong hàm Postgres. Tôi đã thử mã này:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

Và tôi nhận được điều này:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

Và đây là lỗi tôi gặp phải khi thay đổi thành này select * from quote_ident($1) tab where tab.id=1:

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

Có lẽ, quote_ident($1)hoạt động, bởi vì không có where quote_ident($1).id=1phần tôi nhận được 1, có nghĩa là một cái gì đó đã được chọn. Tại sao quote_ident($1)tác phẩm đầu tiên có thể làm việc và tác phẩm thứ hai không cùng một lúc? Và làm thế nào điều này có thể được giải quyết?


Tôi biết câu hỏi này hơi cũ, nhưng tôi đã tìm thấy nó trong khi tìm kiếm câu trả lời cho một vấn đề khác. Không thể hàm của bạn chỉ truy vấn thông tin_schema? Ý tôi là, theo một cách nào đó, đó là cách - để cho phép bạn truy vấn và xem những đối tượng nào tồn tại trong cơ sở dữ liệu. Chỉ là một ý tưởng.
David S

@DavidS Cảm ơn bạn đã nhận xét, tôi sẽ thử.
John Doe

Câu trả lời:


124

Điều này có thể được đơn giản hóa và cải thiện hơn nữa:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

Gọi với tên đủ điều kiện giản đồ (xem bên dưới):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

Hoặc là:

SELECT some_f('"my very uncommon table name"');

Những điểm chính

  • Sử dụng một OUTtham số để đơn giản hóa hàm. Bạn có thể chọn trực tiếp kết quả của SQL động vào nó và được thực hiện. Không cần thêm các biến và mã.

  • EXISTSlàm chính xác những gì bạn muốn. Bạn nhận được truenếu hàng tồn tại hoặc falsekhác. Có nhiều cách khác nhau để làm điều này, EXISTSthường là hiệu quả nhất.

  • Có vẻ như bạn muốn lấy lại một số nguyên , vì vậy tôi truyền booleankết quả từ EXISTSsang integer, kết quả mang lại chính xác những gì bạn có. Tôi sẽ trả về boolean để thay thế.

  • Tôi sử dụng kiểu định danh đối tượng làm kiểu regclassđầu vào cho _tbl. Điều đó làm được tất cả mọi thứ quote_ident(_tbl)hoặc format('%I', _tbl)sẽ làm, nhưng tốt hơn, bởi vì:

  • .. nó cũng ngăn chặn việc tiêm SQL .

  • .. nó không thành công ngay lập tức và duyên dáng hơn nếu tên bảng không hợp lệ / không tồn tại / ẩn đối với người dùng hiện tại. (Một regclasstham số chỉ có thể áp dụng cho các bảng hiện có .)

  • .. nó hoạt động với các tên bảng đủ điều kiện giản đồ, trong đó một bảng đơn giản quote_ident(_tbl)hoặc format(%I)sẽ không thành công vì chúng không thể giải quyết sự mơ hồ. Bạn sẽ phải chuyển và thoát khỏi tên lược đồ và bảng riêng biệt.

  • Tôi vẫn sử dụng format(), bởi vì nó đơn giản hóa cú pháp (và để chứng minh cách nó được sử dụng), nhưng %sthay vì %I. Thông thường, các truy vấn phức tạp hơn nên format()sẽ giúp ích nhiều hơn. Đối với ví dụ đơn giản, chúng ta cũng có thể nối:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Không cần xác định idcột trong bảng trong khi chỉ có một bảng duy nhất trong FROMdanh sách. Không có sự mơ hồ nào có thể có trong ví dụ này. (Động) Các lệnh SQL bên trong EXECUTEcó một phạm vi riêng biệt , các biến hoặc tham số hàm không hiển thị ở đó - trái ngược với các lệnh SQL thuần túy trong thân hàm.

Đây là lý do tại sao bạn luôn thoát khỏi đầu vào của người dùng cho SQL động đúng cách:

db <> fiddle ở đây minh họa SQL injection
Old sqlfiddle


2
@suhprano: Chắc chắn rồi. Hãy thử nó:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter

tại sao lại là% s chứ không phải% L?
Lotus

3
@Lotus: Lời giải thích có trong câu trả lời. regclassgiá trị được thoát tự động khi xuất dưới dạng văn bản. %Lsẽ sai trong trường hợp này.
Erwin Brandstetter

CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql; tạo một hàm đếm hàng trong bảng,select table_rows('nf_part1');
l mingzhi

làm thế nào chúng ta có thể nhận được tất cả các cột?
Ashish

12

Nếu có thể, đừng làm điều này.

Đó là câu trả lời - đó là một kiểu chống đối. Nếu máy khách biết bảng mà nó muốn có dữ liệu, thì SELECT FROM ThatTable. Nếu một cơ sở dữ liệu được thiết kế theo cách mà điều này được yêu cầu, nó có vẻ như được thiết kế dưới mức tối ưu. Nếu một lớp truy cập dữ liệu cần biết liệu một giá trị có tồn tại trong bảng hay không, thì việc soạn SQL trong mã đó rất dễ dàng và việc đẩy mã này vào cơ sở dữ liệu là không tốt.

Đối với tôi, điều này có vẻ giống như việc lắp đặt một thiết bị bên trong thang máy, nơi người ta có thể gõ số tầng mong muốn. Sau khi nút Go được nhấn, nó sẽ di chuyển một bàn tay cơ học đến đúng nút của tầng mong muốn và nhấn nó. Điều này dẫn đến nhiều vấn đề tiềm ẩn.

Xin lưu ý: không có ý định chế nhạo, ở đây. Ví dụ về thang máy ngớ ngẩn của tôi là * thiết bị tốt nhất mà tôi có thể tưởng tượng * để chỉ ra ngắn gọn các vấn đề với kỹ thuật này. Nó thêm một lớp vô dụng của hướng, di chuyển lựa chọn tên bảng từ không gian người gọi (sử dụng DSL, SQL mạnh mẽ và được hiểu rõ) thành một kết hợp sử dụng mã SQL phía máy chủ khó hiểu / kỳ lạ.

Việc phân chia trách nhiệm như vậy thông qua chuyển động của logic xây dựng truy vấn thành SQL động làm cho mã khó hiểu hơn. Nó vi phạm một quy ước tiêu chuẩn và đáng tin cậy (cách truy vấn SQL chọn những gì cần chọn) dưới tên mã tùy chỉnh đầy tiềm ẩn lỗi.

Dưới đây là điểm chi tiết về một số vấn đề tiềm ẩn với phương pháp này:

  • SQL động cung cấp khả năng chèn SQL mà khó có thể nhận ra trong mã giao diện người dùng hoặc mã kết thúc sau (người ta phải kiểm tra chúng cùng nhau để xem điều này).

  • Các thủ tục và hàm được lưu trữ có thể truy cập tài nguyên mà chủ sở hữu SP / chức năng có quyền nhưng người gọi thì không. Theo như tôi hiểu, nếu không có sự quan tâm đặc biệt, thì theo mặc định khi bạn sử dụng mã tạo ra SQL động và chạy nó, cơ sở dữ liệu thực thi SQL động theo quyền của người gọi. Điều này có nghĩa là bạn sẽ không thể sử dụng các đối tượng đặc quyền hoặc bạn phải mở chúng cho tất cả các máy khách, làm tăng diện tích bề mặt của cuộc tấn công tiềm ẩn đối với dữ liệu đặc quyền. Đặt SP / chức năng tại thời điểm tạo để luôn chạy với tư cách người dùng cụ thể (trong SQL Server, EXECUTE AS) có thể giải quyết vấn đề đó, nhưng làm cho mọi thứ phức tạp hơn. Điều này làm trầm trọng thêm nguy cơ tiêm SQL được đề cập ở điểm trước, bằng cách biến SQL động trở thành một vectơ tấn công rất hấp dẫn.

  • Khi một nhà phát triển phải hiểu mã ứng dụng đang làm gì để sửa đổi nó hoặc sửa lỗi, anh ta sẽ thấy rất khó để thực thi truy vấn SQL chính xác. Trình biên dịch SQL có thể được sử dụng, nhưng điều này có các đặc quyền đặc biệt và có thể có tác động tiêu cực đến hiệu suất trên hệ thống sản xuất. Truy vấn được thực thi có thể được ghi lại bởi SP nhưng điều này làm tăng độ phức tạp vì lợi ích đáng ngờ (yêu cầu cung cấp các bảng mới, xóa dữ liệu cũ, v.v.) và khá không rõ ràng. Trên thực tế, một số ứng dụng được cấu trúc sao cho nhà phát triển không có thông tin đăng nhập cơ sở dữ liệu, do đó, hầu như anh ta không thể thực sự thấy truy vấn đang được gửi.

  • Khi xảy ra lỗi, chẳng hạn như khi bạn cố gắng chọn một bảng không tồn tại, bạn sẽ nhận được một thông báo dọc theo dòng "tên đối tượng không hợp lệ" từ cơ sở dữ liệu. Điều đó sẽ xảy ra hoàn toàn giống nhau cho dù bạn đang soạn SQL ở back end hay cơ sở dữ liệu, nhưng sự khác biệt là, một số nhà phát triển kém đang cố gắng khắc phục sự cố hệ thống phải kéo sâu thêm một cấp vào một hang khác bên dưới hang nơi vấn đề tồn tại, để đào sâu vào thủ tục kỳ diệu Có Tất cả để cố gắng tìm ra vấn đề là gì. Nhật ký sẽ không hiển thị "Lỗi trong GetWidget", nó sẽ hiển thị "Lỗi trong OneProcedureToRuleThemAllRunner". Sự trừu tượng này nói chung sẽ làm cho một hệ thống tồi tệ hơn .

Một ví dụ trong pseudo-C # về chuyển đổi tên bảng dựa trên một tham số:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

Mặc dù điều này không loại bỏ mọi vấn đề có thể có trong tưởng tượng, nhưng những sai sót mà tôi đã nêu ra với kỹ thuật khác không có trong ví dụ này.


4
Tôi không hoàn toàn đồng ý với điều đó. Giả sử, bạn nhấn nút "Bắt đầu" này và sau đó một số cơ chế kiểm tra xem sàn có tồn tại hay không. Các hàm có thể được sử dụng trong trình kích hoạt, do đó có thể kiểm tra một số điều kiện. Mô tả này có thể không phải là đẹp nhất, nhưng nếu hệ thống đã đủ lớn và bạn cần thực hiện một số chỉnh sửa trong logic của nó, thì, lựa chọn này không quá ấn tượng, tôi cho là vậy.
John Doe

1
Nhưng hãy cân nhắc rằng hành động cố gắng nhấn một nút không tồn tại sẽ đơn giản tạo ra một ngoại lệ cho dù bạn xử lý nó như thế nào. Bạn thực sự không thể nhấn một nút không tồn tại, vì vậy, không có lợi gì khi thêm, bên cạnh việc nhấn nút, một lớp để kiểm tra các số không tồn tại, vì mục nhập số đó không tồn tại trước khi bạn tạo lớp nói trên! Theo tôi, trừu tượng là công cụ mạnh nhất trong lập trình. Tuy nhiên, thêm một lớp chỉ sao chép kém một phần trừu tượng hiện có là sai . Bản thân cơ sở dữ liệu đã là một lớp trừu tượng ánh xạ tên cho các tập dữ liệu.
ErikE,

3
Tại chỗ trên. Toàn bộ điểm của SQL là thể hiện tập dữ liệu mà bạn muốn trích xuất. Điều duy nhất mà hàm này làm là đóng gói một câu lệnh SQL "đóng hộp". Với thực tế là mã nhận dạng cũng được mã hóa cứng nên toàn bộ thứ có mùi khó chịu.
Nick Hristov

1
@three Cho đến khi ai đó ở trong giai đoạn thành thạo (xem mô hình thu nhận kỹ năng của Dreyfus ) một kỹ năng, anh ta chỉ nên tuân thủ tuyệt đối các quy tắc như "KHÔNG chuyển tên bảng vào một thủ tục được sử dụng trong SQL động". Ngay cả việc ám chỉ rằng không phải lúc nào cũng xấu cũng là lời khuyên tồi . Biết điều này, người mới bắt đầu sẽ bị cám dỗ để sử dụng nó! Điều đó thật xấu. Chỉ những bậc thầy của một chủ đề mới nên phá vỡ các quy tắc, vì họ là những người duy nhất có kinh nghiệm để biết trong mọi trường hợp cụ thể liệu việc phá vỡ quy tắc đó có thực sự có ý nghĩa hay không.
ErikE

1
@ ba ly Tôi đã cập nhật rất nhiều chi tiết về lý do tại sao đó là một ý tưởng tồi.
ErikE

10

Bên trong mã plpgsql, câu lệnh EXECUTE phải được sử dụng cho các truy vấn trong đó tên bảng hoặc cột đến từ các biến. Ngoài ra, IF EXISTS (<query>)cấu trúc không được phép khi queryđược tạo động.

Đây là chức năng của bạn với cả hai sự cố đã được khắc phục:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;

Cảm ơn bạn, tôi đã trả lời như vậy vài phút trước khi đọc câu trả lời của bạn. Sự khác biệt duy nhất là tôi phải xóa quote_ident()vì nó thêm dấu ngoặc kép, điều này làm tôi ngạc nhiên một chút, tốt, vì nó được sử dụng trong hầu hết các ví dụ.
John Doe

Những dấu ngoặc kép đó sẽ cần thiết nếu / khi tên bảng chứa các ký tự bên ngoài [az] hoặc nếu / khi nó xung đột với một số nhận dạng dành riêng (ví dụ: "nhóm" làm tên bảng)
Daniel Vérité

Và, nhân tiện, bạn có thể vui lòng cung cấp một liên kết chứng minh rằng IF EXISTS <query>cấu trúc đó không tồn tại không? Tôi khá chắc chắn rằng tôi đã nhìn thấy một cái gì đó giống như một mẫu mã làm việc.
John Doe

1
@JohnDoe: IF EXISTS (<query>) THEN ...là một cấu trúc hoàn toàn hợp lệ trong plpgsql. Chỉ không với SQL động cho <query>. Tôi sử dụng nó rất nhiều. Ngoài ra, chức năng này có thể được cải thiện một chút. Tôi đã đăng một câu trả lời.
Erwin Brandstetter

1
Xin lỗi, bạn nói đúng if exists(<query>), nó hợp lệ trong trường hợp chung. Chỉ cần kiểm tra và sửa đổi câu trả lời cho phù hợp.
Daniel Vérité

4

Đầu tiên không thực sự "hoạt động" theo nghĩa của bạn, nó chỉ hoạt động khi nó không tạo ra lỗi.

Hãy thử SELECT * FROM quote_ident('table_that_does_not_exist');, và bạn sẽ thấy tại sao hàm của bạn trả về 1: vùng chọn trả về một bảng có một cột (được đặt tên quote_ident) với một hàng (biến $1hoặc trong trường hợp cụ thể này table_that_does_not_exist).

Những gì bạn muốn làm sẽ yêu cầu SQL động, đây thực sự là nơi chứa các quote_*hàm được sử dụng.


Cảm ơn rất nhiều, Matt, table_that_does_not_existđã đưa ra kết quả tương tự, bạn nói đúng.
John Doe

2

Nếu câu hỏi là để kiểm tra xem bảng có trống hay không (id = 1), thì đây là phiên bản đơn giản của proc được lưu trữ của Erwin:

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;

1

Tôi biết đây là một chủ đề cũ, nhưng tôi đã xem qua nó gần đây khi cố gắng giải quyết vấn đề tương tự - trong trường hợp của tôi, đối với một số tập lệnh khá phức tạp.

Chuyển toàn bộ tập lệnh thành SQL động không phải là lý tưởng. Đó là công việc tẻ nhạt và dễ xảy ra lỗi, và bạn mất khả năng tham số hóa: các tham số phải được nội suy thành các hằng số trong SQL, gây hậu quả xấu cho hiệu suất và bảo mật.

Đây là một thủ thuật đơn giản cho phép bạn giữ nguyên SQL nếu bạn chỉ cần chọn từ bảng của mình - sử dụng SQL động để tạo một dạng xem tạm thời:

CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer
AS $$
BEGIN
    drop view if exists myview;
    execute format('create temporary view myview as select * from %s', _tbl);
    -- now you can reference myview in the SQL
    IF EXISTS (select * from myview where myview.id=1) THEN
     return 1;
    END IF;
    return 0;
END;
$$ language plpgsql;

0

Nếu bạn muốn tên bảng, tên cột và giá trị được chuyển động cho hàm dưới dạng tham số

sử dụng mã này

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value

-2

Tôi có phiên bản 9.4 của PostgreSQL và tôi luôn sử dụng mã này:

CREATE FUNCTION add_new_table(text) RETURNS void AS
$BODY$
begin
    execute
        'CREATE TABLE ' || $1 || '(
        item_1      type,
        item_2      type
        )';
end;
$BODY$
LANGUAGE plpgsql

Và sau đó:

SELECT add_new_table('my_table_name');

Nó hoạt động tốt cho tôi.

Chú ý! Ví dụ trên là một trong những ví dụ hiển thị "Làm thế nào không nếu chúng tôi muốn giữ an toàn trong khi truy vấn cơ sở dữ liệu": P


1
Tạo một newbảng khác với thao tác với tên của một bảng hiện có. Dù bằng cách nào, bạn nên thoát khỏi các tham số văn bản được thực thi dưới dạng mã hoặc bạn đang mở SQL injection.
Erwin Brandstetter

Ồ, phải, sai lầm của tôi. Chủ đề đã đánh lừa tôi và thêm vào đó tôi đã không đọc nó đến cuối. Bình thường trong trường hợp của tôi. : P Tại sao mã có tham số văn bản lại bị tiêm vào?
dm3

Rất tiếc, nó thực sự nguy hiểm. Cảm ơn bạn đã trả lời!
dm3
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.