کنترل با توابع PostgreSQL: بستن شکاف به عملکرد ORM

پرده برداری از تفاوت: درک شکاف بین درایور مستقیم و عملکرد ORM
زمانی که نوبت به انتخاب فناوری برای توسعه backend و دستکاری داده ها در پایگاه داده مانند PostgreSQL می رسد، توسعه دهندگان اغلب با انتخاب بین استفاده از درایور رسمی یا لایه انتزاعی مانند ORM، در این مورد، Prisma روبرو هستند. هر دو رویکرد مزایا و معایب خود را دارند، اما استدلال شده است که ORM ها به دلیل ویژگی های بی شماری که برای عملیات معمول CRUD که معمولاً در یک REST API انجام می شوند، برتر هستند. در این مقاله، بررسی خواهیم کرد که چگونه توابع در PostgreSQL به ما این امکان را می دهند که فاصله بین استفاده مستقیم از درایور رسمی و عملکردهای ارائه شده توسط یک ORM مانند Prisma را پر کنیم. ما به طور خاص بر روی عملیات درج داده تمرکز خواهیم کرد و بررسی خواهیم کرد که چگونه توابع می توانند نتایجی مشابه با نتایج بدست آمده در هنگام استفاده از یک ORM ارائه دهند، از جمله توانایی برگرداندن اشیاء ایجاد شده و موارد دیگر.
import 'dotenv/config';
import { Client } from 'pg';
const pg = new Client({
user: process.env.DB_USER,
password: process.env.DB_PASS,
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10),
database: process.env.DB_NAME,
});
pg.connect().then(() => {
console.log('db connected');
pg.query(
`INSERT INTO reservations (_date, hora, res_number, res_name, room, meal_plan, pax_number, cost, observations)
VALUES
('2023-07-27', '21:00', 001, 'Jhon Doe', 'P01', 'SC', 2, 50.00, 'Sin observaciones')`,
).then((result) => {
console.log(result);
pg.end().then(() => {
console.log('disconnected from db');
});
});
});
/*
db connected
Result {
command: 'INSERT',
rowCount: 1,
oid: 0,
rows: [],
fields: [],
... more metada
}
disconnected from db
*/
در این مثال که در Nodejs اجرا می شود، می بینید که اطلاعات مفید زیادی در مورد نتیجه آن عملیات وجود ندارد و ابرداده های زیادی وجود دارد.
حالا بیایید آن را با یک insert run در Prisma مقایسه کنیم.
prisma.pokemons.create({
data:{
id: 1100,
name: 'new_pokemon'
}
}).then((result: any) => {
console.log(result);
})
/*
{ id: 1100, name: 'new_pokemon' }
*/
با Prisma می توانیم از شر ابرداده خلاص شویم و شی درج شده در پایگاه داده را استخراج کنیم
بیایید سعی کنیم آن نوع نتیجه را تقلید کنیم.
مقدمه ای برای عملکرد در Postgresql.
بیایید مستقیماً به یک مثال کد ساده از یک تابع در Postgres برویم.
CREATE FUNCTION sum(num1 numeric, num2 numeric) -- declare function and parameteres
RETURNS numeric -- returned type
AS $$
BEGIN -- starting the code
RETURN num1 + num2; -- result
END; -- end of code
$$ LANGUAGE plpgsql; -- end of the function
SELECT sum(2, 3); -- returns 5
ممکن است بخش های بیشتری در اعلان تابع وجود داشته باشد، مانند گرفتن استثناها یا اعلام متغیرها. آن را برای بعد می گذاریم.
اما حالا بیایید به مثال دیگری بپردازیم. کمی مفصل تر
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
pax INTEGER,
capacity INTEGER,
do_res DATE, -- date of reservation
to_res TIME -- time of reservation
);
-- populating database with bad data that will throw exceptions in our function
INSERT INTO reservations (pax, capacity, do_res, to_res) VALUES
(0, 0, '2022-01-01', '10:00:00');
/* Use it to play with the function if needed
-- populating database with good data
INSERT INTO reservations (pax, capacity, do_res, to_res) VALUES
(2, 4, '2022-01-01', '10:00:00'),
(3, 6, '2022-01-02', '11:00:00'),
(4, 8, '2022-01-03', '12:00:00'),
(5, 10, '2022-01-04', '13:00:00'),
(6, 12, '2022-01-05', '14:00:00'),
(7, 14, '2022-01-06', '15:00:00'),
(8, 16, '2022-01-07', '16:00:00'),
(9, 18, '2022-01-08', '17:00:00'),
(10, 20, '2022-01-09', '18:00:00');
*/
بیایید سعی کنیم تابعی ایجاد کنیم که بتواند درصد اشغال را مشخص کند.
SELECT SUM(pax)*100 / SUM(capacity) AS ocupation FROM reservations;
این یک پرس و جو عادی است اما اکنون اجازه دهید تابع را تولید کنیم
میدانم که میتوانیم از یک NULLIF برای اجتناب از استثناها استفاده کنیم، اما میخواهم به زودی این استثنا را پرتاب کنم.
CREATE OR REPLACE FUNCTION porcentage_ocupation(fecha_i DATE, fecha_f DATE)
RETURNS FLOAT AS $$
DECLARE -- declaring variables
total_pax INTEGER;
total_capacity INTEGER;
porcentage FLOAT;
BEGIN
SELECT SUM(pax), SUM(capacity) INTO total_pax, total_capacity FROM reservations WHERE do_res BETWEEN fecha_i AND fecha_f;
porcentage := total_pax * 100.0 / total_capacity;
RETURN porcentage;
END;
$$ LANGUAGE plpgsql;
اکنون ما یک تابع پیچیده تر ایجاد کرده ایم اما این هنوز کافی نیست
بسیار متداول و تمرین خوبی است که به سرورها اجازه دهید هر نوع خطا را مدیریت کنند. اگر خطایی در داخل پایگاه داده رخ دهد، به سروری تبدیل می شود که یک استثنا ایجاد می کند. اگر سرور را فقط با داده های بد باقی مانده پر کنید و سعی کنید عملکرد را اجرا کنید، این همان چیزی است که به دست خواهید آورد.
SELECT porcentage_ocupation('2022-01-01', '2022-12-31');
-- ERROR: division by zero
-- CONTEXT: PL/pgSQL function porcentaje_ocupation(date,date) line 8 at assignment
من پیشنهاد نمی کنم که باید روش متفاوتی برای رسیدگی به این استثناها ایجاد کنیم، اما اگر بخواهیم می توانیم. در قسمت بعدی استثناها را مدیریت می کنیم و یک JSON را با داده ها و ابرداده هایی که می خواهیم از پایگاه داده ارائه کنیم، برمی گردانیم.
ارتقاء آن: افزایش قابلیتهای عملکرد با پاسخهای سفارشی و مدیریت استثنا
و اکنون زمان آن است که برخی از ویژگیهای دیگر را که میتوانیم در Postgresql استفاده کنیم، مانند یک بلوک try/catch در postgresql و برخی از دادههای سیستمی که میتوانیم در داخل توابع دریافت کنیم، به شما ارائه کنیم.
CREATE OR REPLACE FUNCTION porcentage_ocupation2(fecha_i DATE, fecha_f DATE)
RETURNS JSON AS $$ -- returning a JSON is a great option to return customized results
DECLARE
total_pax INTEGER;
total_capacity INTEGER;
porcentage FLOAT;
stack text; -- will get the stack of errors here
result JSON; -- will build the result here
BEGIN -- begin the function
BEGIN -- this second begin works as the try in a try/catch block
SELECT SUM(pax), SUM(capacity) INTO total_pax, total_capacity FROM reservations WHERE do_res BETWEEN fecha_i AND fecha_f;
porcentage := total_pax * 100.0 / total_capacity;
result := json_build_object('isError', FALSE, 'result', porcentage);
EXCEPTION -- and here is the catch in the try/catch block
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS stack = PG_EXCEPTION_CONTEXT; -- how we can obtain the stack of the error in postgresql
result := json_build_object('isError', TRUE, 'message', SQLERRM, 'errorCode', SQLSTATE, 'stack', stack); -- SQLERRM, SQLSTATE are system variables provided by postgresql
END; -- end of the try/catch block
RETURN result; -- returning the response
END;
$$ LANGUAGE plpgsql;
و با ایجاد این تابع دوم، اجازه میدهیم نتیجه را وقتی به ۰ تقسیم کنیم، ببینیم.
SELECT porcentage_ocupation2('2022-01-01','2022-12-31');
porcentage_ocupation2
----------------------
{"isError" : true, "message" : "division by zero", "errorCode" : "22012", "stack" : "PL/pgSQL function porcentage_ocupation2(date,date) line 11 at assignment"}
همانطور که قبلاً گفتم، من از این دفاع نمیکنم که آیا این یک عمل خوب است یا نه، فقط میگویم که میتوانیم استثناهایی را که در اجرای کد رخ میدهند مدیریت کنیم. این تصمیم مدیر پروژه یا توسعه دهندگان ارشد خواهد بود که آیا این مفید است یا خیر.
بیایید به کنترلر در Nodejs بپریم، زیرا به پیش نیازهای رسیدن به هدف خود رسیده ایم.
pg.connect().then(() => {
console.log('db connected');
pg.query(`SELECT porcentage_ocupation2('2022-01-01','2022-12-31' as result)`).then(
(result) => {
console.log(result.rows[0].result); // looking for the result straight to the point it should be
pg.end().then(() => {
console.log('disconnected from db');
});
},
);
});
/*
db connected
{
isError: true,
message: 'division by zero',
errorCode: '22012',
stack: 'PL/pgSQL function porcentage_ocupation2(date,date) line 11 at assignment'
}
disconnected from db
*/
ما از این واقعیت استفاده می کنیم که با بازگرداندن یک JSON از تابع Postgres، 1 و تنها 1 ردیف را برمی گرداند. به همین دلیل است که ما همیشه می توانیم نتیجه را در آن مکان از جسم جستجو کنیم. این دقیقا همان ویژگی است که ما می خواستیم بسازیم. حالا بیایید به جلو برویم تا آن را روی عملیات CRUD پیاده سازی کنیم، زیرا از انجام آن در SELECT QUERY معنادارتر است.
تبدیل مفاهیم به کد: نوشتن منطق INSERT و UPDATE در توابع PostgreSQL
ما در حال اتمام هستیم پس بیایید دیگر وقت را تلف نکنیم.
CREATE OR REPLACE FUNCTION create_reservation(
_pax INTEGER,
_capacity INTEGER,
_do_res DATE,
_to_res TIME
) RETURNS JSON AS $$
DECLARE
inserted_reservation reservations; -- here will retrived the inserted object
response JSON;
stack_info TEXT;
BEGIN
BEGIN
IF _do_res < CURRENT_DATE THEN
response := json_build_object(
'isError', FALSE, -- It could be considered an error (a bad request error)
'message', 'Bad request: No record inserted - You cant create a reservation in the past',
'rowsAffected', 0,
'result', NULL
);
ELSE
BEGIN
INSERT INTO reservations (
pax,
capacity,
do_res,
to_res
) VALUES (
_pax,
_capacity,
_do_res,
_to_res
)
RETURNING * INTO inserted_reservation; -- retriving the insert object
IF inserted_reservation IS NULL THEN
response := json_build_object(
'isError', TRUE, 'message', 'No record inserted',
'rowsAffected', 0
);
ELSE
response := json_build_object(
'isError', FALSE, 'result', inserted_reservation, 'rowsAffected', 1
);
END IF;
EXCEPTION
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS stack_info = PG_EXCEPTION_CONTEXT;
response := json_build_object(
'isError', TRUE, 'message', SQLERRM, 'errorCode', SQLSTATE,
'stack', stack_info
);
END;
END IF;
RETURN response;
END;
END;
$$ LANGUAGE plpgsql;
SELECT create_reservation(
2, 4, '2023-07-27', '19:00'
) AS result;
به خاطر داشته باشید که این تابع یک “محدودیت” را به create_reservation اضافه می کند و اجازه ایجاد رزرو قبل از CURRENT_DATE را نمی دهد. این یک کنترل اضافی است که می توانیم با استفاده از توابع اضافه کنیم.
با این کار به پایان این پست رسیدیم.
در نتیجه، با استفاده از قدرت توابع در PostgreSQL، ما با موفقیت شکاف بین ORM و برنامه نویسی پایگاه داده مستقیم را کاهش دادیم. از طریق کاوش خود، ما به درک عمیق تری از نحوه استفاده موثر از توابع دست یافته ایم که به ما امکان می دهد پاسخ ها را سفارشی کنیم، استثناها را مدیریت کنیم و کنترل بیشتری در پایگاه داده خود اضافه کنیم.
در حالی که کار مستقیم با پایگاه داده ممکن است به تلاش بیشتری نیاز داشته باشد، کنترل بی نظیری بر فرآیند دستکاری داده ها ارائه می دهد. توابع به توسعه دهندگان قدرت می دهد تا کنترل دقیق داشته باشند و از پتانسیل کامل PostgreSQL استفاده کنند.
لحظه ای که توابع PostgreSQL واقعاً از ORM ها پیشی می گیرند، زمانی است که سرور با چالش اجرای پرس و جوهای پیچیده یا ارائه هوش تجاری مواجه می شود. در این سناریوها است که قدرت واقعی برنامه نویسی تابع در PostgreSQL آشکار می شود. توابع سطحی از انعطافپذیری و کنترل را ارائه میدهند که فراتر از قابلیتهای ORM است و به توسعهدهندگان اجازه میدهد تا پرس و جوهای پیچیده و بهینهسازی شده را متناسب با نیازهای تجاری خاص ایجاد کنند.