افزایش قابلیتهای PostgreSQL: راهنمای کامل انواع داده سفارشی (UDT)
PostgreSQL به عنوان یک سیستم مدیریت پایگاه داده قدرتمند و انعطافپذیر، با قابلیتهای پیشرفتهای از جمله پشتیبانی از انواع داده تعریفشده توسط کاربر (User-Defined Types – UDTs)، خود را از رقبا متمایز میکند. این ویژگی به توسعهدهندگان امکان میدهد تا انواع دادهای را ایجاد کنند که کاملاً با نیازهای خاص برنامههایشان مطابقت داشته باشد، فراتر از انواع داده داخلی استاندارد. استفاده از UDTs نه تنها به بهبود مدلسازی دادهها کمک میکند، بلکه میتواند عملکرد و وضوح کد را نیز افزایش دهد، زیرا دادهها به شکلی طبیعیتر و قابل فهمتر در پایگاه داده ذخیره و پردازش میشوند. این مقاله به بررسی چگونگی تعریف و استفاده از انواع داده سفارشی در PostgreSQL میپردازد، از تعریف یک نوع داده ساده تا افزودن توابع، عملگرها و تبدیلهای نوع (casts) برای تعامل کامل با آن.
برای شروع، بیایید یک نوع داده جدید به نام `POINT` را در PostgreSQL تعریف کنیم که برای ذخیره مختصات دو بعدی (x, y) استفاده میشود. این مثال ساده، بنیاد ساخت UDTهای پیچیدهتر را فراهم میکند. هر نوع داده جدید به یک روش داخلی برای تبدیل ورودی متنی به فرم باینری داخلی و یک روش خروجی برای تبدیل فرم باینری به متنی نیاز دارد.
PostgreSQL دو فرم برای دستور `CREATE TYPE` ارائه میدهد. فرم اول برای تعریف انواع مرکب (composite types) است که مشابه یک سطر جدول عمل میکنند. فرم دوم که برای تعریف انواع پایه (base types) مانند `POINT` استفاده میشود، به یک نام نوع و توابع ورودی/خروجی داخلی نیاز دارد.
(input_type, output_type)
برای تعریف توابع `point_in` و `point_out` که ورودی و خروجی نوع `POINT` را مدیریت میکنند، میتوانیم از زبان C استفاده کنیم، زیرا این کارایی بالایی را فراهم میکند و امکان مدیریت دقیق فرمت داخلی دادهها را میدهد. کد C برای این توابع در یک فایل به نام `point.c` کامپایل شده و در پایگاه داده بارگذاری میشود.
“`c
// point.c
#include “postgres.h”
#include “fmgr.h”
#include “utils/builtins.h”
PG_MODULE_MAGIC;
typedef struct Point
{
double x;
double y;
} Point;
PG_FUNCTION_INFO_V1(point_in);
Datum
point_in(PG_FUNCTION_ARGS)
{
char *str = PG_GETARG_CSTRING(0);
double x, y;
if (sscanf(str, “(%lf,%lf)”, &x, &y) != 2)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg(“invalid input syntax for point: \”%s\””,
str)));
Point *result = (Point *)palloc(sizeof(Point));
result->x = x;
result->y = y;
PG_RETURN_POINTER(result);
}
PG_FUNCTION_INFO_V1(point_out);
Datum
point_out(PG_FUNCTION_ARGS)
{
Point *point = (Point *)PG_GETARG_POINTER(0);
char *result = psprintf(“(%g,%g)”, point->x, point->y);
PG_RETURN_CSTRING(result);
}
“`
پس از کامپایل این کد C به یک فایل کتابخانه اشتراکی (مانند `point.so` یا `point.dll`)، میتوانیم آن را در PostgreSQL با استفاده از دستورات `CREATE FUNCTION` و سپس `CREATE TYPE` بارگذاری کنیم:
CREATE FUNCTION point_in(cstring)
RETURNS point
AS '$libdir/point', 'point_in'
LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION point_out(point)
RETURNS cstring
AS '$libdir/point', 'point_out'
LANGUAGE C IMMUTABLE STRICT;
CREATE TYPE point (
INTERNALLENGTH = 16,
INPUT = point_in,
OUTPUT = point_out
);
دستور `INTERNALLENGTH = 16` نشان میدهد که نوع `POINT` 16 بایت فضا اشغال میکند (دو مقدار double، هر یک 8 بایت). `INPUT` و `OUTPUT` نیز توابعی را که برای تبدیل رشته به فرم داخلی و بالعکس استفاده میشوند، مشخص میکنند.
حالا که نوع `POINT` را تعریف کردهایم، میتوانیم از آن در جداول و کوئریها استفاده کنیم. برای مثال، ایجاد یک جدول و درج دادهها:
CREATE TABLE my_points (id serial, location point);
INSERT INTO my_points (location) VALUES ('(1.0,2.0)'), ('(3.0,4.0)');
SELECT * FROM my_points;
خروجی کوئری نشان خواهد داد که PostgreSQL به درستی مقادیر `POINT` را ذخیره و نمایش میدهد.
id | location
----+----------
1 | (1,2)
2 | (3,4)
(2 rows)
برای اینکه نوع `POINT` کاربردیتر شود، میتوانیم عملگرهایی را برای آن تعریف کنیم. عملگرها به ما امکان میدهند تا با نوع دادههای سفارشی خود به روشهای شهودیتری کار کنیم، مثلاً جمع دو `POINT` یا مقایسه آنها.
برای تعریف یک عملگر، از دستور `CREATE OPERATOR` استفاده میکنیم. این دستور نیازمند مشخص کردن تابع پیادهسازی کننده عملگر، آرگومانهای چپ و راست، و ویژگیهایی مانند `COMMUTATOR` (عملگر جابجاییپذیر) و `HASHES` (قابلیت هش شدن) است.
بیایید یک عملگر `+` برای جمع دو `POINT` و یک عملگر `=` برای مقایسه آنها تعریف کنیم. ابتدا، توابع C مربوطه را ایجاد میکنیم:
“`c
// point.c (ادامه)
PG_FUNCTION_INFO_V1(point_add);
Datum
point_add(PG_FUNCTION_ARGS)
{
Point *p1 = (Point *)PG_GETARG_POINTER(0);
Point *p2 = (Point *)PG_GETARG_POINTER(1);
Point *result = (Point *)palloc(sizeof(Point));
result->x = p1->x + p2->x;
result->y = p1->y + p2->y;
PG_RETURN_POINTER(result);
}
PG_FUNCTION_INFO_V1(point_eq);
Datum
point_eq(PG_FUNCTION_ARGS)
{
Point *p1 = (Point *)PG_GETARG_POINTER(0);
Point *p2 = (Point *)PG_GETARG_POINTER(1);
PG_RETURN_BOOL(p1->x == p2->x && p1->y == p2->y);
}
“`
پس از کامپایل مجدد فایل `point.c` و بارگذاری مجدد کتابخانه، توابع را در PostgreSQL تعریف میکنیم:
CREATE FUNCTION point_add(point, point) RETURNS point
AS '$libdir/point', 'point_add' LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION point_eq(point, point) RETURNS boolean
AS '$libdir/point', 'point_eq' LANGUAGE C IMMUTABLE STRICT;
حالا میتوانیم عملگرهای `+` و `=` را تعریف کنیم:
CREATE OPERATOR + (
LEFTARG = point,
RIGHTARG = point,
FUNCTION = point_add,
COMMUTATOR = +
);
CREATE OPERATOR = (
LEFTARG = point,
RIGHTARG = point,
FUNCTION = point_eq,
COMMUTATOR = =,
HASHES
);
عملگر `COMMUTATOR` نشان میدهد که ترتیب آرگومانها در عملگر `+` بر نتیجه تأثیر نمیگذارد (P1 + P2 = P2 + P1). `HASHES` برای عملگر `=` به PostgreSQL میگوید که این عملگر برای استفاده در جداول هش و ایندکسهای هش مناسب است.
با تعریف این عملگرها، میتوانیم آنها را در کوئریهای خود استفاده کنیم:
SELECT '(1,2)'::point + '(3,4)'::point;
SELECT location FROM my_points WHERE location = '(1,2)'::point;
PostgreSQL همچنین امکان تعریف توابع SQL را برای نوع دادههای سفارشی فراهم میکند. این توابع میتوانند بر روی یک یا چند آرگومان از نوع `POINT` عمل کنند و نتایجی از همان نوع یا انواع دیگر را برگردانند.
برای مثال، میتوانیم یک تابع برای جمع دو `POINT` بدون نیاز به C تعریف کنیم:
CREATE FUNCTION add_point(a POINT, b POINT) RETURNS POINT AS $$
SELECT ('(' || ($1.x + $2.x) || ',' || ($1.y + $2.y) || ')')::POINT;
$$ LANGUAGE SQL IMMUTABLE STRICT;
این تابع با استفاده از ساختار داخلی `POINT` (که توسط توابع `point_in` و `point_out` تعریف شده است) به مقادیر `x` و `y` دسترسی پیدا میکند و یک `POINT` جدید را برمیگرداند.
با این تابع، میتوانیم به سادگی دو نقطه را جمع کنیم:
SELECT add_point('(1,2)'::point, '(3,4)'::point);
تبدیل نوع (Casting) امکان تبدیل یک نوع داده به نوع دیگر را فراهم میکند. این ویژگی برای تعامل با انواع داده استاندارد یا دیگر UDTها بسیار مفید است. برای مثال، میتوانیم یک `POINT` را به `TEXT` و بالعکس تبدیل کنیم.
یک `CAST` به صورت زیر تعریف میشود:
(source_type AS target_type)
برای تعریف یک تبدیل از `POINT` به `TEXT`، میتوانیم از تابع `point_out` استفاده کنیم:
CREATE CAST (point AS text)
WITH FUNCTION point_out(point)
AS IMPLICIT;
و برای تبدیل `TEXT` به `POINT`، از تابع `point_in` استفاده میکنیم:
CREATE CAST (text AS point)
WITH FUNCTION point_in(cstring)
AS IMPLICIT;
کلمه کلیدی `IMPLICIT` به PostgreSQL میگوید که این تبدیلها میتوانند به صورت خودکار در شرایط مناسب اعمال شوند. با این تبدیلها، میتوانیم `POINT`ها را به `TEXT` تبدیل کرده و نمایش دهیم یا از رشتهها `POINT` بسازیم:
SELECT 'Hello World' || '(1,2)'::point;
SELECT CAST('(-10.5,20.3)' AS POINT);
استفاده از انواع داده تعریفشده توسط کاربر در PostgreSQL ابزاری قدرتمند برای مدلسازی دادههای پیچیده و افزایش انعطافپذیری پایگاه داده فراهم میکند. با تعریف دقیق توابع ورودی/خروجی، عملگرها، توابع و تبدیلهای نوع، میتوان UDTهایی ایجاد کرد که به طور یکپارچه با سیستم و برنامههای کاربردی شما کار کنند و تجربه توسعه را بهبود بخشند. این قابلیت نه تنها برای دادههای جغرافیایی یا علمی مفید است، بلکه برای هر نوع داده خاص کسبوکار که نیاز به نمایش و پردازش ساختاریافته دارد، کاربرد دارد.