حالت تاریک
فصل ۸ — بازآراییهای یکپارچگی ارجاعی (Referential Integrity Refactorings)
بازآراییهای یکپارچگی ارجاعی (Referential Integrity Refactorings) تغییراتی هستند که اطمینان میدهند ردیف ارجاعشده در جدول دیگر وجود دارد و/یا ردیفی که دیگر لازم نیست بهطور مناسب حذف شود. بازآراییهای یکپارچگی ارجاعی عبارتاند از:
- Add Foreign Key Constraint
- Add Trigger For Calculated Column
- Drop Foreign Key Constraint
- Introduce Cascading Delete
- Introduce Hard Delete
- Introduce Soft Delete
- Introduce Trigger For History
Add Foreign Key Constraint — افزودن محدودیت کلید خارجی
محدودیت کلید خارجی را به جدول موجود اضافه کنید تا رابطه با جدول دیگر اعمال شود.
انگیزه (Motivation)
دلیل اصلی اعمال Add Foreign Key Constraint اعمال وابستگیهای داده در سطح پایگاه داده است — اطمینان از اینکه پایگاه داده برخی قواعد کسبوکار یکپارچگی ارجاعی (RI) را اعمال میکند و از ذخیرهٔ دادهٔ نامعتبر جلوگیری میکند. این بهویژه وقتی چند برنامه به یک پایگاه داده دسترسی دارند مهم است، چون نمیتوانید روی اعمال سازگار قواعد یکپارچگی داده توسط آنها حساب کنید.
معاوضههای بالقوه (Potential Tradeoffs)
محدودیتهای کلید خارجی عملکرد پایگاه داده را کاهش میدهند چون وجود ردیف در جدول خارجی هر بار که ردیف منبع بهروز میشود تأیید میشود. علاوه بر این، هنگام افزودن محدودیت کلید خارجی باید دربارهٔ ترتیب insert، update و delete دقت کنید. مثلاً در شکل ۸.۱، نمیتوانید ردیفی در Account بدون ردیف متناظر در AccountStatus اضافه کنید. پیامد این است که برنامه — یا لایهٔ persistence — باید از وابستگیهای جدول در پایگاه داده آگاه باشد. خوشبختانه بسیاری از پایگاههای داده اعمال محدودیتها در زمان commit را امکانپذیر میکنند و به شما اجازه میدهند ردیفها را به هر ترتیبی insert/update یا delete کنید تا یکپارچگی داده در زمان commit حفظ شود. این نوع قابلیت توسعه را آسانتر میکند و انگیزهٔ بیشتری برای استفاده از محدودیتهای کلید خارجی فراهم میکند.
شکل ۸.۱. افزودن محدودیت کلید خارجی به ستون Account.AccountStatus.
مکانیک بهروزرسانی طرحواره (Schema Update Mechanics)
همانطور که در شکل ۸.۱ نشان داده شده، برای بهروزرسانی طرحواره و افزودن محدودیت کلید خارجی باید موارد زیر را انجام دهید:
راهبرد بررسی محدودیت را انتخاب کنید. بسته به محصول پایگاه داده، یک یا دو روش برای اعمال کلیدهای خارجی پشتیبانی میشود. اول، با روش بررسی فوری (immediate checking)، محدودیت کلید خارجی هنگام insert/update یا delete داده از جدول بررسی میشود. این روش در شکست سریعتر بهتر است و شما را وادار میکند به ترتیب تغییرات پایگاه داده (insert، update و delete) فکر کنید. دوم، با روش بررسی معوق (deferred checking)، محدودیت کلید خارجی هنگام commit تراکنش از برنامه بررسی میشود. این روش انعطاف میدهد که نگران ترتیب تغییرات پایگاه داده نباشید چون محدودیتها در زمان commit بررسی میشوند. این رویکرد همچنین به شما امکان میدهد همهٔ اشیای dirty را cache کنید و بهصورت batch بنویسید — فقط باید در زمان commit پایگاه داده در وضعیت تمیز باشد. در هر صورت، پایگاه داده هنگام اولین شکست محدودیت کلید خارجی استثنا برمیگرداند. (ممکن است چندین مورد باشد.)
محدودیت کلید خارجی را ایجاد کنید. محدودیت کلید خارجی را در پایگاه داده با بند
ADD CONSTRAINTعبارتALTER TABLEایجاد کنید. محدودیت پایگاه داده باید طبق قراردادهای نامگذاری پایگاه داده نامگذاری شود تا وضوح و گزارش خطای مؤثر توسط پایگاه داده فراهم شود. اگر از بررسی محدودیتها در زمان commit استفاده میکنید، ممکن است افت عملکرد داشته باشید چون پایگاه داده یکپارچگی داده را در زمان commit بررسی میکند — مشکل مهمی برای جداول با میلیونها ردیف.index برای کلید اصلی جدول خارجی معرفی کنید (اختیاری). پایگاههای داده از عبارتهای select روی جداول ارجاعشده برای تأیید معتبر بودن دادهٔ واردشده در جدول فرزند استفاده میکنند. اگر ستون
AccountStatus.StatusCodeindex نداشته باشد، ممکن است افت عملکرد قابلتوجهی تجربه کنید و نیاز به اعمال بازآرایی Introduce Index (صفحهٔ ۲۴۸) داشته باشید. با ایجاد index، عملکرد بررسی محدودیت افزایش مییابد، اما عملکرد update، insert و delete روی AccountStatus کاهش مییابد چون پایگاه داده باید index اضافهشده را نگه دارد.
کد زیر گامهای افزودن محدودیت کلید خارجی به جدول را نشان میدهد. در این مثال، محدودیت را طوری ایجاد میکنیم که بلافاصله پس از تغییر داده بررسی شود:
sql
ALTER TABLE Account
ADD CONSTRAINT FK_Account_AccountStatus
FOREIGN KEY (StatusCode)
REFERENCES AccountStatus;در این مثال، محدودیت کلید خارجی را طوری ایجاد میکنیم که در زمان commit بررسی شود:
sql
ALTER TABLE Account
ADD CONSTRAINT FK_Account_AccountStatus
FOREIGN KEY (StatusCode)
REFERENCES AccountStatus
INITIALLY DEFERRED;مکانیک مهاجرت داده (Data-Migration Mechanics)
برای پشتیبانی از افزودن محدودیت کلید خارجی به جدول، ممکن است کشف کنید ابتدا باید دادهٔ موجود در پایگاه داده را بهروز کنید. این فرایند چندمرحلهای است:
اطمینان از وجود دادهٔ ارجاعشده. ابتدا باید اطمینان حاصل کنیم ردیفهایی که به آنها ارجاع داده میشود در AccountStatus وجود دارند. باید دادهٔ موجود در Account و AccountStatus را تحلیل کنید تا مشخص شود آیا ردیفهای گمشده در AccountStatus وجود دارد. سادهترین راه مقایسهٔ تعداد ردیفهای Account با تعداد ردیفهای حاصل از join Account و AccountStatus است.
اطمینان از اینکه جدول خارجی همهٔ ردیفهای لازم را دارد. اگر تعدادها متفاوت است، یا ردیفهایی در AccountStatus کم است و/یا مقادیر نادرست در
Account.StatusCodeوجود دارد. ابتدا فهرستی از مقادیر یکتایAccount.StatusCodeبسازید و با فهرست مقادیرAccountStatus.StatusCodeمقایسه کنید. اگر فهرست اول مقادیری دارد که معتبرند اما در فهرست دوم نیستند، AccountStatus باید بهروز شود. دوم، ممکن است هنوز مقادیر معتبری وجود داشته باشد که در هیچ فهرستی نیست اما در دامنهٔ کسبوکار معتبر است. برای شناسایی این مقادیر باید نزدیک با ذینفعان پروژه کار کنید؛ بهتر است تا وقتی واقعاً به ردیفهای داده نیاز پیدا کردند، آنها را اضافه کنید.اطمینان از اینکه ستون کلید خارجی جدول منبع مقادیر معتبر دارد. فهرستهای مرحلهٔ قبل را بهروز کنید. هر تفاوت در فهرست اکنون باید نتیجهٔ مقادیر نامعتبر یا گمشده در ستون
Account.StatusCodeباشد. باید این ردیفها را بهطور مناسب بهروز کنید — یا با اسکریپت خودکار که مقدار پیشفرض تنظیم میکند یا بهصورت دستی.مقدار پیشفرض برای ستون کلید خارجی معرفی کنید. ممکن است اختیاراً نیاز باشد پایگاه داده مقادیر پیشفرض درج کند وقتی برنامههای بیرونی مقداری برای
Account.StatusCodeارائه نمیدهند. بازآرایی Introduce Default Value (صفحهٔ ۱۸۶) را ببینید.
برای مثال شکل ۸.۱، باید اطمینان حاصل کنید داده قبل از افزودن محدودیت کلید خارجی تمیز است؛ اگر نیست، باید داده را بهروز کنید. فرض کنیم برخی ردیفهای Account وضعیتی ندارند یا بخشی از جدول AccountStatus نیستند. در این موقعیت باید ستون Account.Status را به مقدار شناختهشدهای که در AccountStatus وجود دارد بهروز کنید:
sql
UPDATE Account SET Status = 'DORMANT'
WHERE
Status NOT IN (SELECT StatusCode FROM AccountStatus)
AND Status IS NOT NULL;در موارد دیگر، ممکن است Account.Status مقدار null داشته باشد. در این صورت ستون را با مقدار شناختهشده بهروز کنید:
sql
UPDATE Account SET Status = 'NEW'
WHERE Status IS NULL;مکانیک بهروزرسانی برنامهٔ دسترسی (Access Program Update Mechanics)
باید هر برنامهٔ بیرونی که داده در جدولی که محدودیت کلید خارجی اضافه شده را تغییر میدهد شناسایی و بهروز کنید. مسائلی که باید جستجو کنید:
کد RI مشابه. برخی برنامههای بیرونی قاعدهٔ کسبوکار RI را پیادهسازی میکنند که اکنون از طریق محدودیت کلید خارجی در پایگاه داده مدیریت میشود. این کد باید حذف شود.
کد RI متفاوت. برخی برنامههای بیرونی کدی دارند که قواعد کسبوکار RI متفاوتی از آنچه قرار است پیادهسازی کنید اعمال میکند. پیامد این است که یا باید در افزودن این محدودیت کلید خارجی تجدیدنظر کنید چون در سازمان دربارهٔ قاعدهٔ کسبوکار توافق نیست، یا باید کد را برای کار بر اساس این نسخهٔ جدید (از دید آن برنامه) بازنگری کنید.
کد RI ناموجود. برخی برنامههای بیرونی اصلاً از قاعدهٔ کسبوکار RI مربوط به این جداول آگاه نیستند.
همهٔ برنامههای بیرونی باید برای مدیریت هر استثنایی که پایگاه داده در نتیجهٔ محدودیت کلید خارجی جدید پرتاب میکند بهروز شوند. کد زیر نحوهٔ تغییر کد برنامه برای مدیریت استثناهای پرتابشده توسط پایگاه داده را نشان میدهد:
java
// Before code
stmt = conn.prepare(
"INSERT INTO Account(AccountID,StatusCode,Balance) "+
"VALUES (?,?,?)");
stmt.setLong(1,accountId);
stmt.setString(2,statusCode);
stmt.setBigDecimal(3,balance);
stmt.executeUpdate();
// After code
stmt = conn.prepare(
"INSERT INTO Account(AccountID,StatusCode,Balance) "+
"VALUES (?,?,?)");
stmt.setLong(1,accountId);
stmt.setString(2,statusCode);
stmt.setBigDecimal(3,balance);
try {
stmt.executeUpdate();
} catch (SQLException exception){
int errorCode = exception.getErrorCode();
if (errorCode = 2291) {
handleParentRecordNotFoundError();
}
if (errorCode = 2292) {
handleParentDeletedWhenChildFoundError();
}
}Add Trigger For Calculated Column — افزودن trigger برای ستون محاسبهشده
trigger جدیدی معرفی کنید تا مقدار موجود در ستون محاسبهشده بهروز شود. ستون محاسبهشده ممکن است قبلاً با بازآرایی Introduce Calculated Column (صفحهٔ ۸۱) معرفی شده باشد.
انگیزه (Motivation)
دلیل اصلی اعمال Add Trigger For Calculated Column اطمینان از این است که مقدار موجود در ستون هر بار که دادهٔ منبع تغییر میکند بهدرستی بهروز شود. این کار باید توسط پایگاه داده انجام شود تا همهٔ برنامهها مجبور نباشند این کار را انجام دهند.
معاوضههای بالقوه (Potential Tradeoffs)
وقتی ستون محاسبهشده بر الگوریتمهای پیچیده یا دادهای در چند مکان مبتنی است، trigger شما منطق کسبوکار زیادی پیادهسازی میکند. این ممکن است به ناسازگاری با منطق کسبوکار مشابه پیادهسازیشده در برنامهها منجر شود.
دادهٔ منبعی که trigger استفاده میکند ممکن است در محدودهٔ یک تراکنش بهروز شود. اگر trigger شکست بخورد، تراکنش هم شکست میخورد و rollback میشود. این احتمالاً توسط برنامههای بیرونی بهعنوان اثر جانبی ناخواسته تلقی میشود.
وقتی ستون محاسبهشده بر دادهٔ همان جدول مبتنی است، ممکن است نتوان از trigger برای بهروزرسانی استفاده کرد چون بسیاری از محصولات پایگاه داده این را مجاز نمیکنند.
مکانیک بهروزرسانی طرحواره (Schema Update Mechanics)
اعمال Add Trigger For Calculated Column میتواند بهدلیل وابستگیهای دادهٔ ستون محاسبهشده پیچیده باشد. باید موارد زیر را انجام دهید:
- تعیین کنید آیا triggerها میتوانند ستون محاسبهشده را بهروز کنند. در شکل ۸.۲، ستون TotalPortfolioValue محاسبهشده است — از خط تیره (/) جلوی نام میدانید، قرارداد UML. وقتی TotalPortfolioValue و دادهٔ منبع در همان جدول هستند، احتمالاً نمیتوانید از trigger برای بهروزرسانی مقدار داده استفاده کنید.
شکل ۸.۲. افزودن trigger برای محاسبهٔ Customer.TotalPortfolioValue.
دادهٔ منبع را شناسایی کنید. باید دادهٔ منبع و نحوهٔ استفاده از آن برای تعیین مقدار ستون محاسبهشده را شناسایی کنید.
جدول حاوی ستون را شناسایی کنید. باید مشخص کنید کدام جدول باید ستون محاسبهشده را شامل شود اگر از قبل وجود ندارد. از خود بپرسید ستون محاسبهشده بهترین توصیف کدام موجودیت کسبوکار است. مثلاً شاخص ریسک اعتباری مشتری بیشتر به موجودیت Customer مربوط است.
ستون را اضافه کنید. اگر ستون وجود ندارد، با دستور
ALTER TABLE ADD COLUMNاضافه کنید. از Introduce New Column (صفحهٔ ۳۰۱) استفاده کنید.trigger(ها) را اضافه کنید. باید trigger را به هر جدولی که دادهٔ منبع مرتبط با محاسبهٔ مقدار دارد اضافه کنید. در این مورد، دادهٔ منبع TotalPortfolioValue در جداول Account و InsurancePolicy وجود دارد. بنابراین به trigger برای هر جدول — UpdateCustomerTotalPortfolioValue و UpdateTotalPortfolioValue — نیاز داریم.
کد زیر نحوهٔ افزودن دو trigger را نشان میدهد:
sql
-- Update the TotalPortfolioValue with the trigger.
CREATE OR REPLACE TRIGGER
UpdateCustomerTotalPortfolioValue
AFTER UPDATE OR INSERT OR DELETE
ON Account
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
UpdateCustomerWithPortfolioValue;
END;
END;
/
CREATE OR REPLACE TRIGGER
UpdateCustomerTotalPortfolioValue
AFTER UPDATE OR INSERT OR DELETE
ON InsurancePolicy
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
UpdateCustomerWithPortfolioValue;
END;
END;
/مکانیک مهاجرت داده (Data-Migration Mechanics)
بهمعنای دقیق دادهای برای مهاجرت وجود ندارد، اگرچه مقدار Customer.TotalPortfolioValue باید بر اساس محاسبه پر شود. این کار معمولاً یکبار بهصورت batch با یک یا چند اسکریپت انجام میشود. در مثال ما، باید Customer.TotalPortfolioValue را برای همهٔ ردیفهای موجود در جدول Customer با مجموع Account.Balance و Policy.Value برای هر مشتری بهروز کنیم:
sql
UPDATE Customer SET TotalPortfolioValue =
(SELECT SUM(Account.Balance) + SUM(Policy.Balance)
FROM Account, CustomerInsurancePolicy,
InsurancePolicy
WHERE
Account.AccountID =
CustomerInsurancePolicy.AccountID
AND CustomerInsurancePolicy.PolicyID=Policy.PolicyID
AND Account.CustomerID=Customer.CustomerID
);مکانیک بهروزرسانی برنامهٔ دسترسی (Access Program Update Mechanics)
باید همهٔ جاهایی در برنامههای بیرونی که این محاسبه فعلاً انجام میشود را شناسایی کنید و آن کد را برای کار با TotalPortfolioValue بازنگری کنید — معمولاً شامل حذف کد محاسبه و جایگزینی با خواندن از پایگاه داده است. ممکن است کشف کنید محاسبه در برنامههای مختلف به روشهای متفاوت انجام میشود — یا بهدلیل باگ یا موقعیت متفاوت — و نیاز به مذاکرهٔ محاسبهٔ مناسب با کسبوکار دارید.
Drop Foreign Key Constraint — حذف محدودیت کلید خارجی
محدودیت کلید خارجی را از جدول موجود حذف کنید تا رابطه با جدول دیگر دیگر توسط پایگاه داده اعمال نشود.
انگیزه (Motivation)
دلیل اصلی اعمال Drop Foreign Key Constraint این است که دیگر وابستگیهای داده در سطح پایگاه داده اعمال نشود — در عوض، یکپارچگی داده توسط برنامههای بیرونی اعمال میشود. این بهویژه وقتی مهم است که هزینهٔ عملکرد اعمال RI توسط پایگاه داده دیگر قابل تحمل نیست و/یا قواعد RI بین برنامهها متفاوت است.
معاوضههای بالقوه (Potential Tradeoffs)
معاوضهٔ اساسی عملکرد در برابر کیفیت است: محدودیتهای کلید خارجی معتبر بودن داده را در سطح پایگاه داده تضمین میکنند بههزینهٔ اعمال محدودیت هر بار که دادهٔ منبع بهروز میشود. وقتی Drop Foreign Key را اعمال میکنید، برنامهها در خطر معرفی دادهٔ نامعتبر هستند اگر قبل از نوشتن در پایگاه داده داده را اعتبارسنجی نکنند.
مکانیک بهروزرسانی طرحواره (Schema Update Mechanics)
برای حذف محدودیت کلید خارجی، باید دستور ALTER TABLE DROP CONSTRAINT را اعمال کنید یا میتوانید فقط ALTER TABLE DISABLE CONSTRAINT را اعمال کنید. مزیت رویکرد دوم این است که حتی اگر محدودیت اعمال نشود، رابطه مستند میماند. کد زیر دو روش حذف محدودیت کلید خارجی بین Account.StatusCode و AccountStatus.StatusCode در شکل ۸.۳ را نشان میدهد. روش اول فقط محدودیت را حذف میکند؛ روش دوم آن را غیرفعال میکند و نیاز را مستند میکند. روش دوم را توصیه میکنیم:
شکل ۸.۳. حذف محدودیت کلید خارجی از جدول Account.
sql
ALTER TABLE Account DROP CONSTRAINT FK_Account_Status;
ALTER TABLE Account DISABLE CONSTRAINT FK_Account_Status;مکانیک مهاجرت داده (Data-Migration Mechanics)
برای این بازآرایی پایگاه داده دادهای برای مهاجرت وجود ندارد.
مکانیک بهروزرسانی برنامهٔ دسترسی (Access Program Update Mechanics)
باید هر برنامهٔ بیرونی که داده در ستون(هایی) که محدودیت کلید خارجی روی آنها تعریف شده را تغییر میدهد شناسایی و بهروز کنید. دو مسئله در برنامههای بیرونی وجود دارد. اول، برنامههای فردی باید بهروز شوند تا اطمینان حاصل شود قواعد RI مناسب هنوز اعمال میشوند. قواعد ممکن است متفاوت باشند، اما بهطور کلی باید منطقی به هر برنامه اضافه کنید تا اطمینان حاصل شود هر بار که در Account ارجاع داده میشود ردیفی در AccountStatus وجود دارد. پیامد این است که یا باید تعیین کنید ردیف از قبل وجود دارد — اگر AccountStatus کوچک است، ممکن است cache در حافظه را در نظر بگیرید — و آمادهٔ افزودن ردیف جدید به جدول باشید. مسئلهٔ دوم مدیریت استثنا است. چون پایگاه داده دیگر استثناهای RI مربوط به این محدودیت کلید خارجی پرتاب نمیکند، باید کد برنامهٔ بیرونی را بهطور مناسب بازنگری کنید.
Introduce Cascading Delete — معرفی حذف آبشاری
پایگاه داده بهطور خودکار «ردیفهای فرزند» مناسب را هنگام حذف «ردیف والد» حذف کند.
شکل ۸.۴. معرفی حذف آبشاری روی Policy.
توجه کنید جایگزین حذف ردیفهای فرزند، فقط حذف ارجاع به ردیف والد در ردیفهای فرزند است. این گزینه فقط وقتی قابل استفاده است که ستون(های) کلید خارجی در جداول فرزند مقادیر null را بپذیرند، اگرچه این جایگزین ممکن است به ردیفهای یتیم (orphaned) زیادی منجر شود.
انگیزه (Motivation)
دلیل اصلی اعمال Introduce Cascading Delete حفظ یکپارچگی ارجاعی داده با اطمینان از حذف مناسب ردیفهای مرتبط هنگام حذف ردیف والد است.
معاوضههای بالقوه (Potential Tradeoffs)
سه معاوضهٔ بالقوه با این بازآرایی وجود دارد:
- بنبست (deadlock). هنگام پیادهسازی حذفهای آبشاری باید از وابستگیهای چرخهای اجتناب کنید؛ در غیر این صورت ممکن است بنبست رخ دهد. پایگاههای داده مدرن بنبست را تشخیص میدهند و تراکنشی که باعث بنبست شده rollback میکنند.
- حذف انبوه تصادفی. باید هنگام اعمال این بازآرایی بسیار دقت کنید. مثلاً از نظر تئوری منطقی است که اگر ردیفی در CorporateDivision حذف شود، همهٔ ردیفهای متناظر آن بخش در جدول Employee هم حذف شوند. اما بهراحتی میتوانید هزاران رکورد کارمند را حذف کنید وقتی کسی inadvertently رکوردی نمایندهٔ بخش بزرگی را حذف کند.
- عملکرد تکراری. فریمورکهای persistence مانند Hibernate (www.hibernate.org) و Toplink اوراکل (www.oracle.com) مدیریت روابط بین اشیاء را خودکار میکنند؛ بنابراین ممکن است هنگام اعمال این بازآرایی عملکرد را تکرار کنید.
مکانیک بهروزرسانی طرحواره (Schema Update Mechanics)
برای اعمال Introduce Cascading Delete باید موارد زیر را انجام دهید:
مشخص کنید چه چیزی حذف شود. باید «فرزندان» ردیفی که هنگام حذف ردیف والد باید حذف شوند را شناسایی کنید. مثلاً وقتی سفارش حذف میشود، باید همهٔ اقلام سفارش مرتبط با آن سفارش هم حذف شوند. این فعالیت بازگشتی است — ردیفهای فرزند ممکن است خود فرزندانی داشته باشند که آنها هم باید حذف شوند و شما را به اعمال Introduce Cascading Delete برای آنها هم وادار میکند. توصیه نمیکنیم همه را یکجا اعمال کنید؛ بهتر است این بازآرایی را یک مجموعه جدول در هر بار اعمال کنید، کاملاً پیادهسازی و تست کنید، سپس به بعدی بروید. تغییرات کوچک پیادهسازی آسانتر از تغییرات بزرگ است.
مکانیزم آبشاری را انتخاب کنید. میتوانید حذفهای آبشاری را با trigger یا با محدودیتهای یکپارچگی ارجاعی با گزینهٔ DELETE CASCADE پیادهسازی کنید. (همهٔ فروشندگان پایگاه داده ممکن است این گزینه را پشتیبانی نکنند.)
حذف آبشاری را پیادهسازی کنید. با رویکرد اول، trigger مینویسید که همهٔ فرزندان ردیف والد را هنگام حذف آن حذف کند. این گزینه وقتی بهترین است که کنترل دقیق روی آنچه هنگام حذف ردیف والد حذف میشود میخواهید. نقطهٔ ضعف این است که باید کد بنویسید. همچنین ممکن است موقعیتهای بنبست معرفی کنید اگر روابط متقابل بین چند trigger همزمان اجراشونده را در نظر نگرفته باشید. با رویکرد دوم، محدودیتهای RI با گزینهٔ DELETE CASCADE روشن تعریف میکنید، با دستور SQL
ALTER TABLE MODIFY CONSTRAINT. با انتخاب این گزینه باید محدودیتهای ارجاعی روی پایگاه داده تعریف کنید — اگر از قبل تعریف نشدهاند کار بزرگی است چون باید Add Foreign Key (صفحهٔ ۲۰۴) را تقریباً روی همهٔ روابط پایگاه داده اعمال کنید. مزیت اصلی این گزینه این است که کد جدیدی نمینویسید چون پایگاه داده خودکار ردیفهای فرزند را حذف میکند. چالش این رویکرد این است که debug کردن آن میتواند بسیار سخت باشد.
شکل ۸.۴ نشان میدهد چگونه Introduce Cascade Delete را روی جدول Policy با روش trigger اعمال کنیم. trigger DeletePolicy، که کد آن در زیر آمده، هر ردیفی از جداول PolicyNotes یا Claim مرتبط با ردیفی در جدول Policy که در حال حذف است را حذف میکند:
sql
-- Create trigger to delete the PolicyNotes and Claim.
CREATE OR REPLACE TRIGGER DeletePolicy
AFTER DELETE ON Policy
FOR EACH ROW
DECLARE
BEGIN
DeletePolicyNotes();
DeletePolicyClaims();
END;
/کد زیر نحوهٔ پیادهسازی Introduce Cascade Delete با محدودیتهای RI و گزینهٔ DELETE CASCADE را نشان میدهد:
sql
ALTER TABLE POLICYNOTES ADD CONSTRAINT
FK_DELETEPOLICYNOTES
FOREIGN KEY (POLICYID)
REFERENCES POLICY (POLICYID)
ON DELETE CASCADE
ENABLE
;
ALTER TABLE CLAIMS ADD CONSTRAINT FK_DELETEPOLICYCLAIM
FOREIGN KEY (POLICYID)
REFERENCES POLICY (POLICYID)
ON DELETE CASCADE
ENABLE
;مکانیک مهاجرت داده (Data-Migration Mechanics)
برای این بازآرایی پایگاه داده دادهای برای مهاجرت وجود ندارد.
مکانیک بهروزرسانی برنامهٔ دسترسی (Access Program Update Mechanics)
وقتی این بازآرایی را اعمال میکنید، باید هر کد برنامهای که فعلاً عملکرد حذف فرزندان را پیادهسازی میکند حذف کنید. یک چالش این است که برخی برنامهها حذف را پیادهسازی میکنند و برخی نه — شاید یک برنامه ردیفهای OrderItem را هنگام حذف ردیف Order متناظر حذف کند، اما برنامهٔ دیگر نه. پیادهسازی حذف آبشاری در پایگاه داده ممکن است inadvertently برنامهٔ دوم را تحت تأثیر قرار دهد — شاید برای بهتر. نکته این است که باید بسیار دقت کنید؛ نباید فرض کنید همهٔ برنامهها همان قواعد RI را پیادهسازی میکنند، هرچقدر هم برای شما «واضح» باشد که باید بکنند.
همچنین باید خطاهای جدیدی که پایگاه داده هنگام شکست حذف آبشاری برمیگرداند را مدیریت کنید. کد زیر نحوهٔ تغییر کد برنامه قبل و بعد از اعمال Introduce Cascade Delete را نشان میدهد:
java
// Before code
private void deletePolicy (Policy policyToDelete) {
Iterator policyNotes =
policyToDelete.getPolicyNotes().iterator();
for (Iterator iterator = policyNotes;
iterator.hasNext();)
{
PolicyNote policyNote = (PolicyNote) iterator.next();
DB.remove(policyNote);
}
DB.remove(policyToDelete);
}
// After code
private void deletePolicy (Policy policyToDelete) {
DB.remove(policyToDelete);
}اگر از ابزارهای نگاشت O-R استفاده میکنید، باید فایل نگاشت را تغییر دهید تا گزینهٔ cascade را مشخص کنید:
xml
// After mapping
<hibernate-mapping>
<class name="Policy" table="POLICY">
......
<one-to-many name="policyNotes"
class="PolicyNotes"
cascade="all-delete-orphan"
/>
......
</class>
</hibernate-mapping>Introduce Hard Delete — معرفی حذف فیزیکی
ستون موجودی که نشان میدهد ردیف حذف شده (حذف نرم یا منطقی — soft/logical delete) را حذف کنید و بهجای آن ردیف را واقعاً از برنامه حذف کنید (حذف فیزیکی — hard delete). این بازآرایی مخالف Introduce Soft Delete (صفحهٔ ۲۲۲) است.
انگیزه (Motivation)
دلیل اصلی اعمال Introduce Hard Delete کاهش اندازهٔ جداول است که منجر به عملکرد بهتر و پرسوجوی سادهتر میشود چون دیگر نیازی به بررسی اینکه آیا ردیف بهعنوان حذفشده علامتگذاری شده نیست.
معاوضههای بالقوه (Potential Tradeoffs)
تنها معایب این بازآرایی از دست دادن دادهٔ تاریخی است، اگرچه در صورت نیاز میتوانید از Introduce Trigger For History (صفحهٔ ۲۲۷) استفاده کنید.
مکانیک بهروزرسانی طرحواره (Schema Update Mechanics)
همانطور که شکل ۸.۵ نشان میدهد، برای اعمال Introduce Hard Delete ابتدا باید ستون شناسایی را حذف کنید. باید ستون نشانگر حذف را حذف کنید — بازآرایی Drop Column (صفحهٔ ۷۲) — که در این مورد ستون Customer.isDeleted است. سپس هر کدی — معمولاً کد trigger، اگرچه گاهی کد برنامه — که Customer.isDeleted را بهروز میکند حذف کنید. ممکن است کدی باشد که مقدار اولیه را در مورد نشانگر Boolean به FALSE یا در مورد date/timestamp به تاریخ از پیش تعیینشده تنظیم میکند. این منطق معمولاً با محدودیت ستون مقدار پیشفرض پیادهسازی میشود که همراه با ستون بهطور خودکار حذف میشود. همچنین ممکن است کد trigger باشد که مقدار را به TRUE یا تاریخ/زمان فعلی هنگام تلاش برای حذف ردیف تنظیم میکند. احتمالاً فقط باید trigger را حذف کنید. کد زیر نحوهٔ حذف ستون Customer.isDeleted را نشان میدهد:
sql
ALTER TABLE Customer DROP COLUMN isDeleted;شکل ۸.۵. معرفی حذف فیزیکی برای Customer.
مکانیک مهاجرت داده (Data-Migration Mechanics)
باید همهٔ ردیفهای داده در Customer که isDeleted برابر TRUE است را حذف کنید، چون اینها ردیفهایی هستند که منطقاً حذف شدهاند. قبل از حذف این ردیفها، باید همهٔ دادههایی که به دادهٔ منطقاً حذفشده ارجاع میدهند بهروز و احتمالاً حذف کنید. این کار معمولاً یکبار بهصورت batch با یک یا چند اسکریپت انجام میشود. همچنین باید در نظر بگیرید همهٔ ردیفهای علامتگذاریشده برای حذف را آرشیو کنید تا در صورت نیاز بتوانید از این بازآرایی برگردید. کد زیر نحوهٔ حذف ردیفها از جدول Customer که پرچم Customer.isDeleted برابر TRUE است را نشان میدهد:
sql
-- Delete customers marked for delete
DELETE FROM Customer WHERE isDeleted = TRUE;مکانیک بهروزرسانی برنامهٔ دسترسی (Access Program Update Mechanics)
وقتی بازآرایی Introduce Hard Delete را اعمال میکنید، باید برنامههای بیرونی که به داده دسترسی دارند را در دو روش تغییر دهید. اول، عبارتهای SELECT نباید به ستون Customer.isDeleted ارجاع دهند. دوم، همهٔ کدهای حذف منطقی باید بهروز شوند:
java
// Before code
public void customerDelete(Long customerIdToDelete)
throws Exception {
PreparedStatement stmt = null;
try {
stmt = DB.prepare("UPDATE Customer "+
"SET isDeleted = ? "+
"WHERE CustomerID = ?");
stmt.setLong(1, Boolean.TRUE);
stmt.setLong(2, customerIdToDelete);
stmt.execute();
} catch (SQLException SQLexc){
DB.HandleDBException(SQLexc);
}
finally {DB.cleanup(stmt);}
}
// After code
public void customerDelete(Long customerIdToDelete)
throws Exception {
PreparedStatement stmt = null;
try {
stmt = DB.prepare("DELETE FROM Customer "+
"WHERE CustomerID = ?");
stmt.setLong(1, customerIdToDelete);
stmt.execute();
} catch (SQLException SQLexc){
DB.HandleDBException(SQLexc);
}
finally {DB.cleanup(stmt);}
}Introduce Soft Delete — معرفی حذف نرم
پرچمی به جدول موجود اضافه کنید که نشان دهد ردیف حذف شده (حذف نرم/منطقی) بهجای حذف واقعی ردیف (حذف فیزیکی). این بازآرایی مخالف Introduce Hard Delete (صفحهٔ ۲۱۹) است.
انگیزه (Motivation)
دلیل اصلی اعمال Introduce Soft Delete حفظ همهٔ دادههای برنامه است — معمولاً برای اهداف تاریخی.
معاوضههای بالقوه (Potential Tradeoffs)
عملکرد به دو دلیل ممکن است تحت تأثیر قرار گیرد. اول، پایگاه داده باید همهٔ ردیفهایی که بهعنوان حذفشده علامتگذاری شدهاند را ذخیره کند. این میتواند به استفادهٔ قابلتوجه بیشتر از فضای دیسک و کاهش عملکرد پرسوجو منجر شود. دوم، برنامهها اکنون کار اضافی تمایز بین ردیفهای حذفشده و نشده را انجام میدهند — عملکرد را کاهش میدهد در حالی که پیچیدگی کد را افزایش میدهد.
مکانیک بهروزرسانی طرحواره (Schema Update Mechanics)
همانطور که شکل ۸.۶ نشان میدهد، برای اعمال Introduce Soft Delete باید موارد زیر را انجام دهید:
ستون شناسایی را معرفی کنید. باید ستون جدیدی به Customer اضافه کنید — تبدیل Introduce New Column (صفحهٔ ۳۰۱) — که ردیف را بهعنوان حذفشده یا نشده علامتگذاری میکند. این ستون معمولاً فیلد Boolean است که وقتی ردیف حذف شده مقدار TRUE و در غیر این صورت FALSE دارد، یا date/timestamp که زمان حذف ردیف را نشان میدهد. در مثال ما ستون Boolean isDeleted را معرفی میکنیم. این ستون نباید مقادیر NULL را بپذیرد. (بازآرایی Make Column Non-Nullable را در صفحهٔ ۱۸۹ ببینید.)
تعیین کنید پرچم چگونه بهروز شود. ستون
Customer.isDeletedمیتواند توسط برنامه(ها) یا در پایگاه داده با trigger بهروز شود. رویکرد مبتنی بر trigger را ترجیح میدهیم چون ساده است و خطر اینکه برنامهها ستون را بهدرستی بهروز نکنند را اجتناب میکند.کد حذف را توسعه دهید. کد باید نوشته و تست شود تا ستون نشانگر حذف را هنگام «حذف» ردیف بهطور مناسب بهروز کند. در مورد ستون Boolean مقدار TRUE، در مورد date/timestamp تاریخ و زمان فعلی.
کد درج را توسعه دهید. باید ستون نشانگر حذف را هنگام insert بهطور مناسب تنظیم کنید — FALSE در مورد ستون Boolean و تاریخ از پیش تعیینشده (مثلاً ۱ ژانویه ۵۰۰۰) در مورد date/timestamp. این کار بهراحتی با بازآرایی Introduce Default Value (صفحهٔ ۱۸۶) یا از طریق trigger پیادهسازی میشود.
شکل ۸.۶. معرفی حذف نرم به Customer.
کد زیر نحوهٔ افزودن ستون Customer.isDeleted و اختصاص مقدار پیشفرض FALSE را نشان میدهد:
sql
ALTER TABLE Customer ADD isDeleted BOOLEAN;
ALTER TABLE Customer MODIFY isDeleted DEFAULT FALSE;کد زیر نحوهٔ ایجاد trigger که دستور SQL DELETE را رهگیری میکند و پرچم Customer.isDeleted را TRUE تنظیم میکند را نشان میدهد. کد ردیف داده را قبل از حذف کپی میکند، نشانگر حذف را بهروز میکند، و سپس ردیف را پس از حذف اصلی دوباره در جدول درج میکند:
sql
-- Create a array to store the deleted Customers
CREATE OR REPLACE PACKAGE SoftDeleteCustomerPKG
AS
TYPE ARRAY IS TABLE OF Customer%ROWTYPE INDEX BY
BINARY_INTEGER;
oldvals ARRAY;
empty ARRAY;
END;
/
-- Initialize the array
CREATE OR REPLACE TRIGGER SoftDeleteCustomerBefore
BEFORE DELETE ON Customer
BEGIN
SoftDeleteCustomerPKG.oldvals :=
SoftDeleteCustomerPKG.empty;
END;
/
-- Capture the deleted rows
CREATE OR REPLACE TRIGGER
SoftDeleteCustomerStore
BEFORE DELETE ON Customer
FOR EACH ROW
DECLARE
i NUMBER DEFAULT SoftDeleteCustomerPKG.oldvals.COUNT+1;
BEGIN
SoftDeleteCustomerPKG.oldvals(i).CustomerID :=
:old.CustomerID;
deleteCustomer.oldvals(i).Name := :old.Name;
deleteCustomer.oldvals(i).PhoneNumber :=
:old.PhoneNumber;
END;
/
-- Insert the customers back with the isdeleted flag set to true.
CREATE OR REPLACE TRIGGER SoftDeleteCustomerAdd
AFTER DELETE ON Customer
DECLARE
BEGIN
FOR i IN 1 .. SoftDeleteCustomerPKG.oldvals.COUNT LOOP
insert into Customer(CustomerID,Name,PhoneNumber,isDeleted)
values( deleteCustomer.oldvals(i).CustomerID,
deleteCustomer.oldvals(i).Name,
deleteCustomer.oldvals(i).PhoneNumber,
TRUE);
END LOOP;
END;
/مکانیک مهاجرت داده (Data-Migration Mechanics)
بهمعنای دقیق دادهای برای مهاجرت وجود ندارد، اگرچه مقدار Customer.isDeleted باید هنگام پیادهسازی این بازآرایی در همهٔ ردیفها به مقدار پیشفرض مناسب تنظیم شود. این کار معمولاً یکبار بهصورت batch با یک یا چند اسکریپت انجام میشود.
مکانیک بهروزرسانی برنامهٔ دسترسی (Access Program Update Mechanics)
وقتی بازآرایی Introduce Soft Delete را اعمال میکنید، باید برنامههای بیرونی که به داده دسترسی دارند را تغییر دهید. اول، باید پرسوجوهای خواندن را طوری تغییر دهید که اطمینان حاصل شود دادهٔ خواندهشده از پایگاه داده بهعنوان حذفشده علامتگذاری نشده. برنامهها باید بند WHERE را به همهٔ پرسوجوهای SELECT اضافه کنند، مانند WHERE isDeleted = FALSE. بهجای تغییر همهٔ پرسوجوهای خواندن، میتوانید از بازآرایی Encapsulate Table With View (صفحهٔ ۲۴۳) استفاده کنید تا نما ردیفهایی از Customer را برگرداند که isDeleted برابر TRUE نیست. گزینهٔ دیگر اعمال بازآرایی Add Read Method (صفحهٔ ۲۴۰) است تا بند WHERE مناسب در یک جا پیادهسازی شود.
دوم، باید متدهای حذف را تغییر دهید. همهٔ برنامههای بیرونی باید حذفهای فیزیکی را به بهروزرسانیهایی تبدیل کنند که Customer.isDeleted را بهروز میکنند بهجای حذف فیزیکی ردیف. مثلاً DELETE FROM Customer WHERE PKColumn = nnn به UPDATE Customer SET isDeleted = TRUE WHERE PKColumn = nnn تغییر میکند. بهطور جایگزین، همانطور که قبلاً ذکر شد، میتوانید trigger حذف معرفی کنید که حذف را جلوگیری کرده و Customer.isDeleted را TRUE کند.
کد زیر نحوهٔ تنظیم مقدار اولیهٔ ستون Customer.isDeleted را نشان میدهد:
sql
UPDATE Customer SET
isDeleted = FALSE WHERE isDeleted IS NULL;کد زیر متد خواندن برای یک شیء Customer ساده را قبل و بعد از معرفی بازآرایی Introduce Soft Delete نشان میدهد:
java
// Before code
stmt.prepare(
"SELECT CustomerId, Name, PhoneNumber " +
"FROM Customer" +
"WHERE " +
" CustomerId = ?");
stmt.setLong(1,customer.getCustomerID);
stmt.execute();
ResultSet rs = stmt.executeQuery();
// After code
stmt.prepare(
"SELECT CustomerId, Name, PhoneNumber " +
"FROM Customer" +
"WHERE " +
" CustomerId = ? "+
" AND isDeleted = ?");
stmt.setLong(1,customer.getCustomerID);
stmt.setBoolean(2,false);
stmt.execute();
ResultSet rs = stmt.executeQuery();قطعهٔ کد قبل و بعد نشان میدهد متد حذف پس از معرفی بازآرایی Introduce Soft Delete چگونه تغییر میکند:
java
// Before code
stmt.prepare("DELETE FROM Customer WHERE CustomerID=?");
stmt.setLong(1, customer.getCustomerID);
stmt.executeUpdate();
// After code
stmt.prepare("UPDATE Customer SET isDeleted = ?"+
"WHERE CustomerID=?");
stmt.setBoolean(1, true);
stmt.setLong(2, customer.getCustomerID);
stmt.executeUpdate();Introduce Trigger For History — معرفی trigger برای تاریخچه
trigger جدیدی معرفی کنید تا تغییرات داده را برای اهداف تاریخی یا ممیزی (audit) ثبت کند.
انگیزه (Motivation)
دلیل اصلی اعمال Introduce Trigger For History واگذاری ردیابی تغییرات داده به خود پایگاه داده است. این راهبرد اطمینان میدهد اگر هر برنامهای دادهٔ منبع حیاتی را تغییر دهد، تغییر ردیابی یا ممیزی میشود.
معاوضههای بالقوه (Potential Tradeoffs)
معاوضهٔ اصلی مربوط به عملکرد است — افزودن trigger زمان بهروزرسانی ردیفها در Customer شکل ۸.۷ را افزایش میدهد.
شکل ۸.۷. معرفی trigger تاریخچه برای Customer.
علاوه بر این، ممکن است مجبور شوید برنامهها را بهروز کنید تا اطلاعات زمینهٔ کاربر (user context) را پاس دهند تا پایگاه داده بتواند ثبت کند چه کسی تغییر را انجام داده.
مکانیک بهروزرسانی طرحواره (Schema Update Mechanics)
برای اعمال Introduce Trigger for History باید موارد زیر را انجام دهید:
عملیاتی که تاریخچه برای آنها جمعآوری میشود را تعیین کنید. اگرچه میتوان در Customer داده را insert، update و delete کرد، ممکن است نیاز به ردیابی همهٔ تغییرات نداشته باشید. مثلاً شاید فقط به update و delete علاقهمند باشید نه insertهای اولیه.
ستونهایی که تاریخچه برای آنها جمعآوری میشود را تعیین کنید. باید ستونهای Customer را که میخواهید تغییرات آنها را ردیابی کنید شناسایی کنید. مثلاً شاید فقط بخواهید تغییرات ستون PhoneNumber را ردیابی کنید و نه چیز دیگر.
تعیین کنید دادهٔ تاریخی چگونه ثبت شود. دو انتخاب اساسی دارید: اول، یک جدول عمومی که همهٔ تغییرات دادهٔ تاریخی در پایگاه داده را ردیابی میکند، یا معرفی جدول تاریخچهٔ متناظر برای هر جدولی که میخواهید تاریخچهٔ آن را ثبت کنید. رویکرد تکجدولی برای پایگاههای دادهٔ کوچکتر مناسب است اما بهخوبی مقیاس نمیگیرد.
جدول تاریخچه را معرفی کنید. اگر جدول تاریخچه هنوز وجود ندارد، با دستور
CREATE TABLEایجاد کنید.trigger(ها) را معرفی کنید. trigger(های) مناسب را با دستور
CREATE OR REPLACE TRIGGERمعرفی کنید. این کار میتواند با داشتن trigger که تصویر اصلی ردیف را میگیرد و در جدول CustomerHistory درج میکند انجام شود. راهبرد دوم مقایسهٔ مقادیر قبل و بعد ستونهای مرتبط و ذخیرهٔ توصیف تغییرات در CustomerHistory است.
کد زیر نحوهٔ افزودن جدول CustomerHistory و بهروزرسانی آن از طریق trigger UpdateCustomerHistory را نشان میدهد. تغییر در هر ستون جدول Customer را میگیریم و در CustomerHistory ثبت میکنیم:
sql
-- Create the CustomerHistory table
CREATE TABLE CustomerHistory
(
CustomerID NUMBER,
OldName VARCHAR2(32),
NewName VARCHAR2(32),
ChangedBy NUMBER,
ChangedOn DATE
);
-- Auditing trigger for table Customer
CREATE OR REPLACE TRIGGER
UpdateCustomerHistory
AFTER INSERT OR UPDATE OR DELETE
ON Customer
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
-- Handle Updating of Rows
IF UPDATING THEN
IF NOT (NVL(:OLD.Name = :NEW.Name,FALSE) OR
(:OLD.Name IS NULL AND :NEW.Name IS NULL)) THEN
CreateCustomerHistoryRow( :NEW.CustomerId,
:NEW.Name, :OLD.Name, User);
END IF;
END IF;
-- Handle Deleting of Records
IF DELETING THEN
IF (:OLD.Name IS NOT NULL) THEN
CreateCustomerHistoryRow (:OLD.CustomerId,:OLD.Name,NULL,User);
END IF;
END IF;
IF INSERTING THEN
IF (:NEW.Name IS NOT NULL) THEN
CreateCustomerHistoryRow(:NEW.CustomerId, :NEW.Name,NULL,User);
END IF;
END IF;
END;
/مکانیک مهاجرت داده (Data-Migration Mechanics)
مهاجرت داده معمولاً برای این بازآرایی پایگاه داده لازم نیست. اما اگر میخواهید insertها را ردیابی کنید، ممکن است تصمیم بگیرید برای هر ردیف موجود در Customer یک رکورد تاریخچه ایجاد کنید. این وقتی خوب کار میکند که Customer ستونی برای ثبت تاریخ ایجاد اصلی داشته باشد؛ در غیر این صورت باید مقداری برای تاریخ درج تولید کنید. (سادهترین کار استفاده از تاریخ فعلی است.)
مکانیک بهروزرسانی برنامهٔ دسترسی (Access Program Update Mechanics)
وقتی Introduce Trigger for History را اعمال میکنید، باید موارد زیر را انجام دهید:
تولید تاریخچه توسط برنامه را متوقف کنید. وقتی triggerها برای جمعآوری اطلاعات تاریخی اضافه میکنید، باید هر منطق موجود در برنامههای بیرونی که کد در آن تاریخچه ایجاد میکند را شناسایی و بازنگری کنید.
کد نمایش را بهروز کنید. اگر برنامههای بیرونی فعلاً تاریخچهٔ تولیدشده توسط برنامه را نمایش میدهند، باید برای استفاده از اطلاعات موجود در CustomerHistory بازنگری شوند.
زمینهٔ کاربر را به پایگاه داده ارائه دهید. اگر میخواهید trigger پایگاه داده ثبت کند چه کاربری تغییر داده را انجام داده، باید زمینهٔ کاربر را فراهم کنید. برخی برنامهها — بهویژه آنهایی که با فناوری Web یا n-tier ساخته شدهاند — ممکن است این اطلاعات را ارائه ندهند و باید برای این کار بهروز شوند. بهطور جایگزین، میتوانید وقتی زمینهٔ کاربر ارائه نمیشود مقدار پیشفرض برای آن بدهید.
اگرچه بخشی از خود بازآرایی نیست، تبدیل طرحوارهٔ مرتبط اضافه کردن ستونهایی برای ثبت اینکه چه کسی رکورد را تغییر داده و چه زمانی انجام شده است. راهبرد رایج افزودن ستونهایی مانند UserCreated، CreationDate، UserLastModified و LastModifiedDate به جداول اصلی است (Introduce New Column را در صفحهٔ ۳۰۱ ببینید). دو ستون کاربر شناسهٔ کاربری است که میتواند بهعنوان کلید خارجی به جدول جزئیات کاربر استفاده شود. ممکن است نیاز به افزودن جدول جزئیات کاربر هم داشته باشید (Add Lookup Table را در صفحهٔ ۱۵۳ ببینید).