لینک کوتاه مطلب : https://hsgar.com/?p=5781

حذف نرم احتمالاً ارزشش را ندارد – brandur.org

هرکسی که چند محیط مختلف پایگاه داده تولیدی را دیده باشد احتمالاً با الگوی “حذف نرم” آشنا است – به جای حذف مستقیم داده ها از طریق DELETE بیانیه، جداول اضافه می شود deleted_at برچسب زمانی و حذف با یک عبارت به روز رسانی به جای آن انجام می شود:

UPDATE foo SET deleted_at = now() WHERE id = $1;

مفهوم حذف نرم این است که حذف را ایمن تر و برگشت پذیر می کند. یک بار یک رکورد توسط یک ضربه سخت زده شد DELETE، ممکن است از نظر فنی هنوز با کندن لایه ذخیره سازی قابل بازیابی باشد، اما کافی است بگوییم که بازگرداندن آن واقعاً سخت است. از نظر تئوری با حذف نرم، شما فقط تنظیم می کنید deleted_at بازگشت به NULL و شما تمام کردید:

-- and like magic, it's back!!
UPDATE foo SET deleted_at = NULL WHERE id = $1;

اما این تکنیک دارای معایب عمده ای است. اولین مورد این است که منطق حذف نرم به تمام قسمت های کد شما نفوذ می کند. همه انتخاب های ما چیزی شبیه به این هستند:

SELECT *
FROM customer
WHERE id = @id
    AND deleted_at IS NULL;

و فراموش کردن آن محمول اضافی در deleted_at می تواند عواقب خطرناکی داشته باشد زیرا به طور تصادفی داده هایی را برمی گرداند که دیگر قرار نیست دیده شوند.

برخی از ORM ها یا پلاگین های ORM این کار را با زنجیر کردن خودکار موارد اضافی آسان تر می کنند deleted_at بند در مورد هر پرس و جو (نگاه کنید به acts_as_paranoid برای مثال)، اما صرفاً به این دلیل که پنهان است، لزوماً اوضاع را بهتر نمی کند. اگر یک اپراتور مستقیماً پایگاه داده را پرس و جو کند، احتمالاً فراموش می کند deleted_at زیرا به طور معمول ORM کار را برای آنها انجام می دهد.

یکی دیگر از پیامدهای حذف نرم این است که کلیدهای خارجی به طور موثر گم می شوند.

مزیت اصلی کلیدهای خارجی این است که یکپارچگی ارجاعی را تضمین می کنند. برای مثال، فرض کنید در یک جدول مشتریانی دارید که ممکن است به تعدادی از فاکتورها در جدول دیگر اشاره کنند. بدون کلیدهای خارجی، می‌توانید یک مشتری را حذف کنید، اما فراموش کنید که فاکتورهایش را حذف کنید، در نتیجه تعداد زیادی فاکتور یتیم باقی می‌ماند که به مشتری از بین رفته اشاره می‌کند.

در مورد کلیدهای خارجی، تلاش برای حذف آن مشتری بدون حذف فاکتورها یک خطا است:

ERROR:  update or delete on table "customer" violates
    foreign key constraint "invoice_customer_id_fkey" on table "invoice"

DETAIL:  Key (id)=(64977e2b-40cc-4261-8879-1c1e6243699b) is still
    referenced from table "invoice".

مانند سایر ویژگی های پایگاه داده رابطه ای مانند طرحواره های از پیش تعریف شده، انواع و محدودیت های بررسی، پایگاه داده به معتبر نگه داشتن داده ها کمک می کند.

اما با حذف نرم، این از پنجره خارج می شود. ممکن است یک مشتری به نرمی با آن حذف شود deleted_at پرچم تنظیم شد، اما ما اکنون به فراموشی بازگشته ایم که همین کار را برای فاکتورهای آن انجام دهیم. کلیدهای خارجی آنها هنوز معتبر است زیرا سابقه مشتری از نظر فنی هنوز وجود دارد، اما هیچ بررسی معادلی وجود ندارد که فاکتورها نیز به صورت نرم حذف شده باشند، بنابراین شما می توانید مشتری خود را در حال “حذف” باقی بمانید، اما فاکتورهای آن هنوز زنده هستند.

در چند سال گذشته شاهد پیشرفت‌های عمده‌ای در زمینه حفاظت از داده‌های مصرف‌کننده بوده‌ایم GDPR در اروپا. به این ترتیب، به طور کلی نگه داشتن داده ها به طور بی نهایت مخالف است، که به طور پیش فرض برای ردیف های پاک شده نرم صدق می کند.

بنابراین ممکن است در نهایت متوجه شوید که در حال نوشتن یک فرآیند حذف سخت است که به سوابق حذف شده نرم فراتر از یک افق خاص نگاه می کند و به طور دائم آنها را از پایگاه داده حذف می کند.

اما همان کلیدهای خارجی که حذف نرم‌افزاری عمدتاً بی‌فایده می‌شد، اکنون این کار را دشوارتر می‌کند، زیرا یک رکورد بدون اطمینان از حذف تمام وابستگی‌های آن نیز قابل حذف نیست (ON DELETE CASCADE می تواند این کار را به طور خودکار انجام دهد، اما استفاده از آبشار نسبتاً خطرناک است و برای داده های با وفاداری بالاتر توصیه نمی شود).

خوشبختانه، هنوز هم می‌توانید این کار را در سیستم‌هایی انجام دهید که از CTE‌هایی مانند Postgres پشتیبانی می‌کنند، اما در نهایت با پرس و جوهای بسیار پیچیده‌ای مواجه می‌شوید. در اینجا یک قطعه از موردی است که اخیراً نوشتم که با حذف همه چیز به عنوان بخشی از یک عملیات، همه کلیدهای خارجی را راضی نگه می دارد:

WITH team_deleted AS (
    DELETE FROM team
    WHERE (
        team.archived_at IS NOT NULL
        AND team.archived_at < @archived_at_horizon::timestamptz
    )
    RETURNING *
),

--
-- team resources
--
cluster_deleted AS (
    DELETE FROM cluster
    WHERE team_id IN (
        SELECT id FROM team_deleted
    )
    OR (
        archived_at IS NOT NULL
        AND archived_at < @archived_at_horizon::timestamptz
    )
    RETURNING *
),
invoice_deleted AS (
    DELETE FROM invoice
    WHERE team_id IN (
        SELECT id FROM team_deleted
    )
    OR (
        archived_at IS NOT NULL
        AND archived_at < @archived_at_horizon::timestamptz
    )
    RETURNING *
),

--
-- cluster + team resources
--
subscription_deleted AS (
    DELETE FROM subscription
    WHERE cluster_id IN (
        SELECT id FROM cluster_deleted
    ) OR team_id IN (
        SELECT id FROM team_deleted
    )
    RETURNING *
)

SELECT 'cluster', array_agg(id) FROM cluster_deleted
UNION ALL
SELECT 'invoice', array_agg(id) FROM invoice_deleted
UNION ALL
SELECT 'subscription', array_agg(id) FROM subscription_deleted
UNION ALL
SELECT 'team', array_agg(id) FROM team_deleted;

نسخه خلاصه نشده این پنج برابر طولانی تر است و شامل 30 جدول جداگانه است. جالب است که این کار می کند، اما آنقدر پیچیده است که یک مسئولیت است.

و حتی با آزمایش لیبرال، این نوع پرس‌وجو همچنان می‌تواند به یک مشکل قابلیت اطمینان تبدیل شود، زیرا در صورت اضافه شدن یک وابستگی جدید در آینده اما به‌روزرسانی به پرس و جو فراموش شود، ناگهان پس از یک سال (یا هر چیز دیگری) شروع به شکست می‌کند. افق حذف سخت است) تاخیر.

یک بار دیگر، حذف نرم از نظر تئوری پوششی در برابر از دست دادن تصادفی داده است. به عنوان آخرین استدلال علیه آن، از شما می خواهم به طور واقع بینانه در نظر بگیرید که آیا حذف حذف چیزی است که واقعاً انجام شده است یا خیر.

زمانی که من در Heroku کار می کردم، از حذف نرم استفاده می کردیم.

زمانی که من در Stripe کار می کردم، از حذف نرم استفاده می کردیم.

در کار من در حال حاضر، ما از حذف نرم استفاده می کنیم.

تا جایی که من اطلاع دارم هرگز یک باردر ده سال به علاوه، آیا کسی در هر یک از این مکان‌ها واقعاً از حذف نرم برای حذف چیزی استفاده کرده است.

بزرگترین دلیل آن این است که تقریباً همیشه، حذف داده ها عوارض جانبی غیر داده ای نیز دارد. ممکن است با سیستم های خارجی تماس گرفته شده باشد تا سوابق را در آنجا بایگانی کنند، ممکن است اشیا در فروشگاه های حباب حذف شده باشند یا سرورها از کار افتاده باشند. این فرآیند را نمی توان به سادگی با تنظیم معکوس کرد NULL بر deleted_at – برای تمام آن عملیات دیگر نیز باید undoهای معادل وجود داشته باشد، و آنها به ندرت انجام می دهند.

ما چند مورد در Heroku داشتیم که در آن یک کاربر مهم به طور تصادفی یک برنامه را حذف کرد و می خواست آن را بازیابی کند. ما حذف نرم داشتیم و از نظر تئوری سایر عوارض جانبی حذف می‌توانستند معکوس شوند، اما همچنان تماس گرفتیم که امتحان نکنیم زیرا هیچ‌کس قبلاً این کار را انجام نداده بود، و تلاش برای انجام آن در شرایط اضطراری دقیقاً زمان اشتباهی بود که بتوانیم تشخیص دهیم. آن را از بین می‌بریم – تقریباً مطمئناً دچار اشتباه می‌شویم و کاربر را در وضعیت بدی رها می‌کنیم. در عوض، ما با ایجاد یک برنامه جدید و کمک به آنها برای کپی کردن محیط و داده های برنامه حذف شده در آن به جلو حرکت کردیم. بنابراین حتی در جایی که حذف نرم از نظر تئوری بسیار مفید بود، ما هنوز از آن استفاده نکردیم.

اگرچه من هرگز در عمل کار حذف شده را ندیده‌ام، حذف نرم کاملاً بی‌فایده نبود، زیرا گاهی اوقات از آن برای ارجاع به داده‌های حذف‌شده استفاده می‌کنیم – معمولاً یک فرآیند دستی که در آن شخصی می‌خواهد یک شی حذف شده را برای کمک به یک موضوع ببیند. بلیط پشتیبانی یا تلاش برای له کردن یک اشکال.

و در حالی که من مخالف الگوی حذف نرم سنتی به دلیل نکات منفی ذکر شده در بالا هستم، خوشبختانه یک مصالحه وجود دارد.

به جای نگهداری داده های حذف شده در همان جداولی که از آنها حذف شده است، می توان یک رابطه جدید به طور خاص برای ذخیره همه داده های حذف شده و با یک انعطاف پذیر وجود داشت. jsonb ستون به طوری که بتواند ویژگی های هر جدول دیگری را ثبت کند:

CREATE TABLE deleted_record (
    id uuid PRIMARY KEY DEFAULT gen_ulid(),
    deleted_at timestamptz NOT NULL default now(),
    original_table varchar(200) NOT NULL,
    original_id uuid NOT NULL,
    data jsonb NOT NULL
);

سپس یک حذف به این صورت می شود:

WITH deleted AS (
    DELETE FROM customer
    WHERE id = @id
    RETURNING *
)
INSERT INTO deleted_record
		(original_table, original_id, data)
SELECT 'foo', id, to_jsonb(deleted.*)
FROM deleted
RETURNING *;

این در مقایسه با deleted_at – فرآیند انتخاب ستون ها در jsonb به راحتی قابل برگشت نیست در حالی که انجام این کار ممکن است، احتمالاً شامل ایجاد پرس و جوهای یکباره و مداخله دستی است. اما باز هم، ممکن است اشکالی نداشته باشد – در نظر بگیرید که چند بار واقعاً قصد دارید داده ها را حذف کنید.

این تکنیک تمام مشکلات ذکر شده در بالا را حل می کند:

  • پرس و جوهایی برای داده های عادی و حذف نشده دیگر نیازی به اضافه کردن ندارند deleted_at IS NULL هر کجا.

  • کلیدهای خارجی هنوز کار می کنند. تلاش برای حذف یک رکورد بدون دریافت وابستگی های آن یک خطا است.

  • حذف سخت رکوردهای قدیمی برای نیازهای منظم واقعاً بسیار آسان است: DELETE FROM deleted_record WHERE deleted_at < now() - '1 year'::interval.

دستیابی به داده‌های حذف شده کمی سخت‌تر است، اما خیلی زیاد نیست، و در صورتی که کسی نیاز به نگاه کردن به آن‌ها داشته باشد، همچنان نگه داشته می‌شوند.

مقاله
حذف نرم احتمالاً ارزشش را ندارد

منتشر شده
19 جولای 2022

محل
سانفرانسیسکو

مرا در توییتر پیدا کنید @brandur.

آیا من اشتباه کردم؟ لطفا درنظر داشته باشید ارسال درخواست کشش.



لینک منبع

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.