۱. از فرم روی گوشی به یک منبع مشترک برس
در نمونهٔ «آوای مزرعه»، کاربر فعالیتی مثل مصرف ماده در یک قطعه زمین را ثبت میکند. اگر اطلاعات فقط در وضعیت محلی اپ یا ذخیرهسازی مرورگر همان کاربر بماند، ورود با حساب دیگر لزوماً آن رکورد را نشان نمیدهد. مشکل با اضافهکردن یک صفحهٔ «مدیریت» حل نمیشود: صفحهٔ مدیریت هم باید داده را از منبعی مشترک بخواند.
چیزی که روی گوشی میبینی، یعنی فرم، دکمهٔ ثبت و فهرست رکوردها، فرانتاند است. پایگاه داده و خدماتی که ثبت و خواندن اطلاعات را ممکن میکنند، در سمت بکاند قرار میگیرند. Supabase برای این مرحله یک پایگاه دادهٔ PostgreSQL و امکانات مرتبط در اختیار پروژه میگذارد؛ بنابراین لازم نیست صرفاً برای ذخیره و بازیابی این فرم، از همان ابتدا یک سرور اختصاصی بنویسی. این انتخاب به معنی حذف کامل بکاند نیست: بخشی از مسئولیت بکاند را به سرویس دیگری سپردهای.
نتیجهای که در این مرحله میخواهیم روشن است: دو نفر میتوانند به یک پایگاه داده متصل شوند، اما اینکه هرکدام کدام ردیفها را مجاز است ببیند، هنوز باید جداگانه تعریف شود. مشترکبودن محل ذخیره، بهتنهایی مجوز دسترسی درست ایجاد نمیکند.
۲. پیش از ساخت جدول، مسیر یک رکورد را مشخص کن
فرض کن اپ ابتدا صدا را روی دستگاه نگه میدارد، هنگام فراهمشدن اتصال آن را به متن تبدیل میکند و فیلدهای فرم را از آن استخراج میکند. برای نسخهٔ سادهٔ این طرح، لازم نیست فایل صوتی یا صف کارهای آفلاین را وارد جدول فعالیتها کنی. آنچه به پایگاه داده میرسد فرم تکمیلشده است: مزرعه، نام قطعه، نام ماده، مقدار، واحد و در صورت نیاز متن پیادهشده.
این مرزبندی جلوی یک اشتباه رایج را میگیرد. صف آفلاین برای پیگیری کارهای انجامنشده روی دستگاه است؛ رکورد فعالیت برای نگهداری نتیجهٔ ثبتشده در منبع مشترک. همچنین ذخیرهٔ فایل صوتی با ذخیرهٔ چند فیلد متنی در PostgreSQL یک تصمیم یکسان نیست. اگر بعداً لازم شد مدیر صدای اصلی را هم بررسی کند، باید دربارهٔ نگهداری فایل، هزینه، حریم خصوصی و مدت نگهداریاش جداگانه تصمیم بگیری.
قبل از نوشتن SQL، یک رکورد نمونه روی کاغذ بنویس: «مزرعهٔ نمونه، زمین شمالی، کود آزمایشی، ۱ کیلوگرم». حالا بپرس کدام بخش ممکن است در صدها رکورد تکرار شود. نام و مشخصات مزرعه متعلق به جدول farms است؛ مشخصات شخصی که ثبت میکند به profiles مربوط است؛ خود فعالیت در spray_records مینشیند. نام این جدول را برای همخوانی با نمونهٔ مزرعه نگه میداریم، هرچند فیلد material_name میتواند نام مادهٔ ثبتشده را نگه دارد.
۳. جدول، ردیف و کلید را با هم قاطی نکن
جدول را میتوان برای شروع شبیه یک صفحهٔ گسترده تصور کرد: ستونها نوع اطلاعات را مشخص میکنند و هر ردیف یک مورد ثبتشده است. اما پایگاه داده فقط یک فایل اکسل آنلاین نیست. میتوانی روی نوع داده و قواعد اعتبارسنجی محدودیت بگذاری و بین جدولها رابطهٔ قابلبررسی تعریف کنی.
هر مزرعه یک id دارد. این کلید اصلی یا primary key شناسهٔ یکتای آن ردیف است؛ نام مزرعه برای این کار مناسب نیست، چون ممکن است دو مزرعه نام یکسان داشته باشند یا نامشان تغییر کند. هر فعالیت نیز id خودش را دارد و در ستون farm_id شناسهٔ مزرعهٔ مربوط را نگه میدارد. farm_id یک کلید خارجی یا foreign key است: پایگاه داده بررسی میکند مزرعهای که به آن اشاره کردهای واقعاً وجود داشته باشد.
برای کاربر هم همین الگو را به کار میبریم. operator_id مشخص میکند چه کسی فعالیت را ثبت کرده و reviewed_by، اگر رکورد تأیید شده باشد، به شخص بررسیکننده اشاره میکند. این دو ستون میتوانند هر دو به profiles.id وصل شوند، بیآنکه مجبور شویم نام و ایمیل افراد را در تکتک رکوردهای فعالیت تکرار کنیم. انتظار این مرحله یک نقشهٔ ساده است: farms و profiles هرکدام میتوانند به چند ردیف در spray_records مرتبط باشند.
۴. طرح آموزشی را در SQL Editor بساز
برای اجرای نمونه، به یک پروژهٔ آزمایشی Supabase و دسترسی به SQL Editor آن نیاز داری. کد زیر را در SQL Editor همان پروژه قرار بده و یکبار اجرا کن. این طرح، مثال تکمیلی آموزشی است، نه بازنویسی لفظبهلفظ فایل SQL پروژهٔ نمایشدادهشده. آن را روی پایگاه دادهای که از قبل جدولهای همنام دارد اجرا نکن؛ پیش از تغییر پایگاه دادهٔ موجود، ساختار و دادههایش را بررسی کن.
create table public.farms (
id uuid primary key default gen_random_uuid(),
name text not null check (length(btrim(name)) > 0),
location text,
created_at timestamptz not null default now()
);
create table public.profiles (
id uuid primary key references auth.users(id),
display_name text not null check (length(btrim(display_name)) > 0),
role text not null default 'operator'
check (role in ('operator', 'admin')),
created_at timestamptz not null default now()
);
create table public.spray_records (
id uuid primary key default gen_random_uuid(),
farm_id uuid not null references public.farms(id),
operator_id uuid not null references public.profiles(id),
field_name text not null check (length(btrim(field_name)) > 0),
material_name text not null check (length(btrim(material_name)) > 0),
quantity numeric(12, 2) not null check (quantity > 0),
unit text not null check (unit in ('لیتر', 'کیلوگرم')),
transcription text,
status text not null default 'pending'
check (status in ('pending', 'approved')),
reviewed_by uuid references public.profiles(id),
reviewed_at timestamptz,
created_at timestamptz not null default now(),
check (
(status = 'pending' and reviewed_by is null and reviewed_at is null)
or
(status = 'approved' and reviewed_by is not null and reviewed_at is not null)
)
);
alter table public.farms enable row level security;
alter table public.profiles enable row level security;
alter table public.spray_records enable row level security;
پس از اجرا، باید سه جدول را در Table Editor ببینی. id هر مزرعه و فعالیت بهطور خودکار ساخته میشود؛ profiles.id باید همان شناسهٔ حساب در auth.users باشد. location توضیح اختیاری محل مزرعه، display_name نام نمایشی کاربر و role یکی از دو مقدار «ثبتکننده» یا «مدیر» است. در فعالیت، farm_id و operator_id به مزرعه و پروفایل موجود اشاره میکنند؛ field_name نام قطعه، material_name نام ماده، quantity عدد مثبت و unit واحد همان عدد است. transcription اختیاری و مقدار پیشفرض status برابر pending است؛ reviewed_by و reviewed_at برای رکوردِ بررسینشده خالی میمانند. ستونهای created_at زمان ایجاد را ثبت میکنند. نوع timestamptz یک لحظهٔ زمانی را نگه میدارد و آن را مطابق منطقهٔ زمانی نشست نمایش میدهد، نه اینکه نام منطقهٔ زمانیِ اولیه را حفظ کند.
دو تصمیم مهم در این کد را ببین: مقدار منفی برای quantity پذیرفته نمیشود و رکورد approved نمیتواند بدون بررسیکننده و زمان بررسی ثبت شود. بااینحال، این محدودیتها مجوز تعریف نمیکنند؛ مثلاً هنوز تضمین نمیکنند فقط مدیر بتواند رکوردی را تأیید کند.
۵. یک دادهٔ آزمایشی بساز و نتیجه را ببین
اول مزرعهای آزمایشی اضافه کن. این دستور را جداگانه در SQL Editor اجرا کن؛ returning شناسهٔ ردیف تازه را نشان میدهد. لازم نیست آن شناسه را حدس بزنی یا دستی بسازی.
insert into public.farms (name, location)
values ('مزرعهٔ نمونه', 'محدودهٔ آزمایشی')
returning id, name, location;
برای آزمایش رابطهٔ کاربر، در بخش Authentication پروژه یک حساب آزمایشی متعلق به خودت بساز و UUID آن را از همان بخش بردار. در دستور زیر، UUID_حساب_آزمایشی را با همان UUID جایگزین کن؛ نویسههای جایگزین را به همین شکل اجرا نکن. شناسهٔ پروفایل باید دقیقاً شناسهٔ همان حساب باشد. اگر پروفایل از قبل وجود داشته باشد، on conflict از درج دوباره جلوگیری میکند و returning ردیفی نشان نمیدهد؛ در آن حالت، همان UUID حساب را برای مرحلهٔ بعد نگه دار.
insert into public.profiles (id, display_name)
values ('UUID_حساب_آزمایشی'::uuid, 'ثبتکنندهٔ آزمایشی')
on conflict (id) do nothing
returning id, display_name;
حالا شناسهٔ مزرعه را از خروجی درج farms و شناسهٔ ثبتکننده را از حساب آزمایشی مرحلهٔ قبل بردار. در دستور زیر هر دو جایگزینِ UUID_... را با UUID واقعی همان ردیفها عوض کن و سپس دستور را اجرا کن؛ به این ترتیب، رکورد به «آخرین» مزرعه یا پروفایلِ نامعلوم وصل نمیشود. در اپ واقعی نیز مزرعهٔ انتخابشده و هویت کاربر واردشده باید بهصورت مجاز و قابلبررسی تعیین شوند.
insert into public.spray_records
(farm_id, operator_id, field_name, material_name, quantity, unit)
values
('UUID_مزرعه_آزمایشی'::uuid, 'UUID_حساب_آزمایشی'::uuid,
'زمین شمالی', 'کود آزمایشی', 1, 'کیلوگرم')
returning id, field_name, material_name, quantity, unit, status;
اگر مرحلهٔ آخر هیچ ردیفی برنگرداند، احتمالاً مزرعه یا پروفایلی برای انتخاب وجود ندارد؛ دو جدول را بررسی کن. اگر خطای کلید خارجی دیدی، شناسهای که استفاده شده به ردیف معتبری اشاره نمیکند. اگر خطای محدودیت مقدار گرفتی، مقدار یا واحد را با قواعد جدول مقایسه کن. خروجی موفق باید فعالیت را با وضعیت اولیهٔ pending نشان دهد، نه approved.
۶. با SELECT بفهم چه چیزی ذخیره شده است
SELECT برای خواندن داده است. علامت * یعنی همهٔ ستونها؛ برای بررسی سریع مفید است، ولی وقتی خروجی مشخصی میخواهی بهتر است ستونها را نام ببری. این دو دستور را جداگانه اجرا کن و تفاوت خروجی را ببین:
select * from public.farms;
select field_name, material_name, quantity, unit
from public.spray_records
where status = 'pending';
اولی تمام ستونهای مزرعهها را نشان میدهد. دومی فقط چهار ستون از فعالیتهای در انتظار بررسی را برمیگرداند. where فیلتر است، نه تغییر داده. برای شمارش همین ردیفها هم میتوانی بنویسی:
select count(*) as pending_count
from public.spray_records
where status = 'pending';
حالا یک بررسی کوچک انجام بده: اگر یک فعالیت دیگر با وضعیت pending بسازی، انتظار داری pending_count یک واحد زیاد شود. اگر عدد تغییر نکرد، ابتدا مطمئن شو دستور درج واقعاً ردیفی برگردانده است؛ بازکردن دوبارهٔ فرم یا تازهسازی صفحه، بهخودیخود به معنی ثبت موفق در پایگاه داده نیست.
۷. با JOIN شناسهها را به نام قابلفهم تبدیل کن
در spray_records نام مزرعه و نام ثبتکننده تکرار نشده؛ فقط شناسههایشان ذخیره شده است. برای ساخت خروجی خوانا، JOIN ردیفهای مرتبط را کنار هم قرار میدهد. شرط on میگوید کدام ستونها باید برابر باشند:
select
r.field_name,
r.material_name,
r.quantity,
r.unit,
f.name as farm_name,
operator.display_name as operator_name,
reviewer.display_name as reviewer_name
from public.spray_records as r
join public.farms as f on f.id = r.farm_id
join public.profiles as operator on operator.id = r.operator_id
left join public.profiles as reviewer on reviewer.id = r.reviewed_by;
r، f، operator و reviewer نامهای کوتاهِ همان جدولها در این پرسوجو هستند. جدول profiles دو بار آمده، چون «ثبتکننده» و «بررسیکننده» دو نقش متفاوت در یک فعالیتاند. برای بررسیکننده از left join استفاده کردیم: رکوردی که هنوز بررسی نشده باید همچنان در نتیجه دیده شود، با reviewer_name خالی. اگر اینجا join معمولی بگذاری، فعالیتهای بدون بررسیکننده از خروجی حذف میشوند.
انتظار داری برای رکورد آزمایشی، نام مزرعه و ثبتکننده دیده شوند و ستون بررسیکننده خالی باشد. اگر پرسوجو هیچ خروجی نداشت، اول وجود ردیف در spray_records را بررسی کن؛ سپس رابطههای farm_id و operator_id را نگاه کن. این تمرین نشان میدهد چرا رابطهٔ درست از کپیکردن نامها در هر ردیف قابلاعتمادتر است.
۸. اتصال فرم را از قوانین دسترسی جدا نگه دار
در تمرین فعلی، درج و خواندن را با SQL Editor آزمایش میکنی؛ کد این مقاله هنوز فرم را به Supabase وصل نمیکند. پس از پیادهسازی ورود کاربران و policyهای مناسب RLS، آزمون اتصال فرم این است: رکوردی را در اپ ثبت کن، وجودش را در Table Editor بررسی کن، صفحهٔ اپ را از نو باز کن و مطمئن شو داده از پایگاه داده برمیگردد، نه فقط از حافظهٔ صفحه. اگر در فرم دیده میشود ولی در جدول نیست، ارسال و رسیدگی به خطای ثبت را بررسی کن؛ اگر در جدول هست ولی در اپ نیست، پرسوجو، فیلتر و مجوزهای دسترسی را بررسی کن.
در طرح آموزشی بالا، Row Level Security یا RLS روی جدولها فعال شده اما هیچ policy برای کاربران اپ تعریف نشده است. بنابراین اتصال معمول فرانتاند نباید بتواند با همین کد، آزادانه دادهها را بخواند یا بنویسد؛ SQL Editor با دسترسی مدیریتی ابزار آزمایش طرح است، نه نمونهٔ مجوزهای کاربر عادی. هرگز برای دورزدن این مانع، کلید محرمانهٔ سمت سرور را داخل کد مرورگر یا PWA قرار نده. داشتن ستون role = 'admin' نیز بهتنهایی جلوی تأیید فعالیت توسط دیگران را نمیگیرد.
پیش از استفادهٔ واقعی، ورود کاربران و policyهای RLS را پیادهسازی و آزمایش کن. این سه جدول هنوز نشان نمیدهند کدام کاربر عضو کدام مزرعه است؛ اگر چند مزرعه با کارکنان یا مدیران جدا داری، باید رابطهٔ عضویت مزرعه و کاربر، مثلاً جدول farm_memberships، را نیز طراحی کنی. سیاستها باید هویت ثبتکننده، مجازبودن دسترسی به farm_id و اختیار تغییر status، reviewed_by و reviewed_at را بررسی کنند؛ همچنین کاربر نباید بتواند با ویرایش profiles.role خودش را مدیر کند. حسابها، مزرعه و رکوردهای آزمایشی را از دادهٔ واقعی جدا نگه دار. مستندات Supabase Auth و Supabase RLS راهنمای شروعاند؛ آموزش جداگانهٔ «ورود کاربران و RLS در Supabase» در ۷ نوامبر منتشر میشود؛ آن درس الگوی مالکیتِ یک جدول مستقل را توضیح میدهد و جای طراحی مجوزهای چندمزرعهای را نمیگیرد و تا پیش از پیادهسازی آنها این نمونه آمادهٔ استفادهٔ کاربران نیست.
۹. خروجی تمرینت را مثل یک محصول کوچک بررسی کن
خروجی قابلتحویل این تمرین فقط سه جدول خالی نیست: باید یک مزرعهٔ آزمایشی، یک پروفایل متصل به حساب آزمایشی و یک فعالیت pending داشته باشی؛ سپس بتوانی با SELECT تعداد فعالیتها را بشماری و با JOIN نام مزرعه و ثبتکننده را کنار همان فعالیت نشان بدهی. در آخر، فقط در محیط آزمایشی ردشدن مقدار صفر را امتحان کن: در دستور زیر، هر دو UUID جایگزین را مانند درج موفق قبلی عوض کن و دستور را جداگانه اجرا کن. خطای محدودیت quantity نتیجهٔ موردانتظار است و نباید رکورد تازهای ثبت شود.
insert into public.spray_records
(farm_id, operator_id, field_name, material_name, quantity, unit)
values
('UUID_مزرعه_آزمایشی'::uuid, 'UUID_حساب_آزمایشی'::uuid,
'زمین شمالی', 'کود آزمایشی', 0, 'کیلوگرم');
برای اطمینان، پرسوجوی شمارش بخش ۶ را دوباره اجرا کن؛ شمار ردیفهای pending نباید بر اثر این درج نامعتبر تغییر کرده باشد.
اگر این بررسیها جواب دادند، ساختار اولیهٔ داده را داری: میدانی فرم پس از اتصال باید چه فیلدهایی بفرستد، پایگاه داده بعضی مقدارهای نامعتبر را رد میکند و تو میدانی هر ردیف به کدام مزرعه و کاربر وصل است. گام بعدی، بازکردن دسترسی کنترلشده است، نه برداشتن محدودیتها برای اینکه فرم موقتاً کار کند.
ویدیوی کامل
اگر میخواهی روند کار و آزمونها را هم ببینی، ویدیوی کامل این آموزش اینجاست:


دیدگاهتان را بنویسید
برای نوشتن دیدگاه باید وارد بشوید.