Skip to content

فصل ۶ — بازآرایی‌های ساختاری

بازآرایی‌های ساختاری، همان‌طور که از نامشان پیداست، ساختار جدولی طرحوارهٔ پایگاه دادهٔ شما را تغییر می‌دهند. بازآرایی‌های ساختاری عبارت‌اند از:

  • Drop Column (حذف ستون)
  • Drop Table (حذف جدول)
  • Drop View (حذف نما)
  • Introduce Calculated Column (معرفی ستون محاسباتی)
  • Introduce Surrogate Key (معرفی کلید جایگزین)
  • Merge Columns (ادغام ستون‌ها)
  • Merge Tables (ادغام جداول)
  • Move Column (انتقال ستون)
  • Rename Column (تغییر نام ستون)
  • Rename Table (تغییر نام جدول)
  • Rename View (تغییر نام نما)
  • Replace Large Object (LOB) With Table (جایگزینی شیء بزرگ با جدول)
  • Replace Column (جایگزینی ستون)
  • Replace One-to-Many With Associative Table (جایگزینی یک-به-چند با جدول واسط)
  • Replace Surrogate Key With Natural Key (جایگزینی کلید جایگزین با کلید طبیعی)
  • Split Column (تقسیم ستون)
  • Split Table (تقسیم جدول)

Common Issues When Implementing Structural Refactorings (مسائل رایج هنگام پیاده‌سازی بازآرایی‌های ساختاری)

هنگام پیاده‌سازی بازآرایی‌های ساختاری، هنگام به‌روزرسانی طرحوارهٔ پایگاه داده باید چند مسئلهٔ رایج را در نظر بگیرید، از جمله موارد زیر:

  1. از چرخه‌های trigger اجتناب کنید. باید trigger را طوری پیاده‌سازی کنید که چرخه ایجاد نشود — اگر مقدار یکی از ستون‌های اصلی تغییر کند، Table.NewColumn1..N باید به‌روز شود، اما آن به‌روزرسانی نباید همان به‌روزرسانی را روی ستون‌های اصلی دوباره trigger کند و همین‌طور ادامه یابد. کد زیر نمونه‌ای از همگام نگه‌داشتن مقدار دو ستون را نشان می‌دهد:
sql
CREATE OR REPLACE TRIGGER SynchronizeFirstName
 BEFORE INSERT OR UPDATE
 ON Customer
 REFERENCING OLD AS OLD NEW AS NEW
 FOR EACH ROW
 DECLARE
 BEGIN
 IF INSERTING THEN
          IF :NEW.FirstName IS NULL THEN
           :NEW.FirstName := :NEW.FName;
          END IF;
          IF :NEW.Fname IS NULL THEN
           :NEW.FName := :NEW.FirstName;
          END IF;
         END IF;
         IF UPDATING THEN
           IF NOT(:NEW.FirstName=:OLD.FirstName) THEN
            :NEW.FName:=:NEW.FirstName;
           END IF;
           IF NOT(:NEW.FName=:OLD.FName) THEN
            :NEW.FirstName:=:NEW.FName;
           END IF;
          END IF;
         END;
       /
  1. نماهای شکسته را تعمیر کنید. نماها به بخش‌های دیگر پایگاه داده وابسته‌اند؛ بنابراین وقتی بازآرایی ساختاری اعمال می‌کنید، ممکن است ناخواسته نمایی را بشکنید. تعاریف نما معمولاً به نماهای دیگر و تعاریف جدول وابسته‌اند. مثلاً نمای VCustomerBalance با join کردن جداول Customer و Account تعریف شده تا بر اساس CustomerNumber مجموع موجودی همهٔ حساب‌ها را برای هر مشتری به‌دست آورد. اگر Customer.CustomerNumber را تغییر نام دهید، این نما عملاً شکسته می‌شود.

  2. triggerهای شکسته را تعمیر کنید. triggerها به تعاریف جدول وابسته‌اند؛ بنابراین تغییرات ساختاری مانند تغییر نام یا انتقال ستون می‌تواند trigger را بشکند. مثلاً trigger درج ممکن است دادهٔ ذخیره‌شده در ستون خاصی را اعتبارسنجی کند؛ و اگر آن ستون تغییر کرده باشد، trigger احتمالاً شکسته می‌شود. کد زیر triggerهای شکسته را در Oracle پیدا می‌کند — چیزی که باید به مجموعهٔ تست خود اضافه کنید. هنوز به تست‌های دیگر برای یافتن نقص‌های منطق کسب‌وکار نیاز دارید:

sql
SELECT Object_Name, Status
          FROM User_Objects
WHERE Object_Type='TRIGGER'

AND Status='INVALID';
  1. stored procedureهای شکسته را تعمیر کنید. stored procedureها procedureهای دیگر را فراخوانی می‌کنند و به جداول، نماها و ستون‌ها دسترسی دارند. بنابراین هر بازآرایی ساختاری می‌تواند stored procedure موجود را بشکند. کد زیر stored procedureهای شکسته را در Oracle پیدا می‌کند — چیزی که باید به مجموعهٔ تست خود اضافه کنید. هنوز به تست‌های دیگر برای یافتن نقص‌های منطق کسب‌وکار نیاز دارید:
sql
SELECT Object_Name, Status
          FROM User_Objects
          WHERE Object_Type='PROCEDURE'
AND Status='INVALID';
  1. جداول شکسته را تعمیر کنید. جداول به‌صورت غیرمستقیم از طریق قراردادهای نام‌گذاری به ستون‌های جداول دیگر وابسته‌اند. مثلاً اگر ستون Customer.CustomerNumber را تغییر نام دهید، باید Account.CustomerNumber و Policy.CustomerNumber را هم به‌همین شکل تغییر نام دهید. کد زیر همهٔ جداولی را که نام ستون‌شان شامل متن CUSTOMERNUMBER است در Oracle پیدا می‌کند:
sql
SELECT Table_Name,Column_Name
          FROM User_Tab_Columns
WHERE Column_Name LIKE '%CUSTOMERNUMBER%';
  1. دورهٔ گذار را تعریف کنید. بازآرایی‌های ساختاری در محیط چندبرنامه‌ای به دورهٔ گذار نیاز دارند. باید همان تاریخ‌های حذف را به طرحوارهٔ اصلی که بازآرایی می‌شود و همچنین هر ستون و trigger اختصاص دهید. این تاریخ حذف باید زمان لازم برای به‌روزرسانی برنامه‌های بیرونی که به آن بخش از پایگاه داده دسترسی دارند را در نظر بگیرد.

Drop Column (حذف ستون)

حذف یک ستون از جدول موجود.

شکل ۶.۱. حذف ستون Customer.FavoriteColor.

انگیزه

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

مبادلات بالقوه

ستونی که حذف می‌شود ممکن است دادهٔ ارزشمندی داشته باشد؛ در آن صورت شاید لازم باشد داده حفظ شود. می‌توانید از Move Data (انتقال داده) برای انتقال داده به جدول دیگر و حفظ آن استفاده کنید. در جداول با تعداد زیاد سطر، حذف ستون ممکن است مدت زیادی طول بکشد و جدول را در طول اجرای Drop Column برای به‌روزرسانی در دسترس نگذارد.

مکانیک‌های به‌روزرسانی طرحواره

برای به‌روزرسانی طرحواره و حذف ستون باید کارهای زیر را انجام دهید:

  1. استراتژی حذف را انتخاب کنید. برخی محصولات پایگاه داده ممکن است اجازهٔ حذف ستون را ندهند و مجبورتان کنند جدول موقت بسازید، همهٔ داده را به جدول موقت منتقل کنید، جدول اصلی را حذف کنید، جدول اصلی را بدون آن ستون دوباره بسازید، داده را از جدول موقت برگردانید و سپس جدول موقت را حذف کنید. اگر پایگاه دادهٔ شما راهی برای حذف ستون‌ها دارد، کافی است ستون را با گزینهٔ DROP COLUMN دستور ALTER TABLE حذف کنید.

  2. ستون را حذف کنید. گاهی وقتی حجم داده زیاد است، باید مطمئن شوید Drop Column در زمان معقولی اجرا می‌شود. برای کمینه‌کردن اختلال، حذف فیزیکی ستون را برای زمانی برنامه‌ریزی کنید که جدول کمترین استفاده را دارد. استراتژی دیگر این است که ستون پایگاه داده را به‌عنوان unused علامت بزنید؛ این با گزینهٔ SET UNUSED دستور ALTER TABLE انجام می‌شود. دستور SET UNUSED بسیار سریع‌تر اجرا می‌شود و اختلال را کم می‌کند. سپس می‌توانید ستون‌های unused را در زمان‌های توقف برنامه‌ریزی‌شده حذف کنید. با این گزینه، پایگاه داده ستون را فیزیکی حذف نمی‌کند بلکه آن را از همه پنهان می‌کند.

  3. کلیدهای خارجی را بازکار کنید. اگر FavoriteColor بخشی از کلید اصلی است، باید ستون‌های متناظر را از جداول دیگری که آن را به‌عنوان (بخشی از) کلید خارجی به Customer استفاده می‌کنند هم حذف کنید. باید محدودیت‌های کلید خارجی را در این جداول دوباره بسازید. در این موقعیت، شاید بخواهید قبل از Drop Column بازآرایی‌هایی مانند Introduce Surrogate Key (معرفی کلید جایگزین) یا Replace Surrogate Key With Natural Key (جایگزینی کلید جایگزین با کلید طبیعی) را اعمال کنید تا این بازآرایی ساده‌تر شود.

راهبرد جایگزین برای حذف فیزیکی ستون، پنهان کردن وجود آن با معرفی نمای جدولی است که به FavoriteColor ارجاع نمی‌دهد، از طریق بازآرایی Encapsulate Table With View (کپسوله‌سازی جدول با نما).

در دورهٔ گذار، فقط یک comment به Customer.FavoriteColor اضافه می‌کنید که نشان دهد به‌زودی حذف می‌شود. پس از دورهٔ گذار، ستون را از جدول Customer با دستور ALTER TABLE حذف می‌کنید، همان‌طور که اینجا می‌بینید:

sql
COMMENT ON Customer.FavoriteColor 'Drop date = September 14 2007';

-- On September 14 2004
ALTER TABLE Customer DROP COLUMN FavoriteColor;

اگر از گزینهٔ SET UNUSED استفاده می‌کنید، می‌توانید دستور زیر را به‌کار ببرید تا Customer.FavoriteColor unused شود — یعنی فیزیکی از جدول Customer حذف نشود، اما برای همهٔ کلاینت‌ها در دسترس و قابل‌مشاهده نباشد:

sql
ALTER TABLE Customer SET UNUSED FavoriteColor;

مکانیک‌های مهاجرت داده

برای پشتیبانی از حذف ستون از جدول، ممکن است متوجه شوید باید دادهٔ موجود را حفظ کنید یا برای عملکرد Drop Column برنامه‌ریزی کنید (چون حذف ستون از جدول تغییر داده روی جدول را ممنوع می‌کند). مسئلهٔ اصلی اینجا حفظ داده قبل از حذف ستون است. وقتی ستون موجودی را از جدولی که در تولید بوده حذف می‌کنید، احتمالاً کسب‌وکار از شما می‌خواهد دادهٔ موجود را «برای هر احتمال» حفظ کنید. ساده‌ترین رویکرد ساخت جدول موقت با کلید اصلی جدول مبدأ و ستونی که حذف می‌شود و سپس انتقال دادهٔ مناسب به این جدول موقت است. می‌توانید روش‌های دیگر حفظ داده مانند آرشیو به فایل‌های خارجی را انتخاب کنید.

کد زیر گام‌های حذف ستون Customer.FavoriteColor را نشان می‌دهد. برای حفظ داده، جدول موقت CustomerFavoriteColor می‌سازید که شامل کلید اصلی جدول Customer و ستون FavoriteColor است.

sql
CREATE TABLE CustomerFavoriteColor
  AS SELECT CustomerID, FavoriteColor FROM Customer;

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

باید هر برنامهٔ بیرونی که به Customer.FavoriteColor ارجاع می‌دهد را شناسایی و سپس به‌روز کنید. مسائلی که باید در نظر بگیرید:

  1. بازآرایی کد برای استفاده از منابع دادهٔ جایگزین. برخی برنامه‌های بیرونی ممکن است کدی داشته باشند که هنوز از دادهٔ داخل Customer.FavoriteColor استفاده می‌کند. در این صورت باید منابع دادهٔ جایگزین پیدا شود و کد برای استفاده از آن‌ها بازکار شود؛ وگرنه باید از بازآرایی صرف‌نظر کنید.

  2. باریک‌کردن دستورات SELECT. برخی برنامه‌های بیرونی ممکن است پرس‌وجوهایی داشته باشند که داده را می‌خوانند اما مقدار بازیابی‌شده را نادیده می‌گیرند.

  3. بازآرایی درج و به‌روزرسانی پایگاه داده. برخی برنامه‌های بیرونی ممکن است کدی داشته باشند که برای درج دادهٔ جدید «مقدار ساختگی» در این ستون می‌گذارند — کدی که باید حذف شود. یا برنامه‌ها ممکن است کدی داشته باشند که در درج یا به‌روزرسانی FavoriteColor را بازنویسی نکند. در موارد دیگر ممکن است SELECT * FROM Customer داشته باشید که برنامه انتظار تعداد مشخصی ستون دارد و ستون‌ها را از result set با ارجاع موقعیتی می‌گیرد. این کد برنامه احتمالاً اکنون می‌شکند چون result set دستور SELECT یک ستون کمتر برمی‌گرداند. به‌طور کلی استفاده از SELECT * از هر جدول در برنامه ایدهٔ خوبی نیست. البته مشکل واقعی این است که برنامه از ارجاع موقعیتی استفاده می‌کند — چیزی که خودتان هم باید بازآرایی آن را در نظر بگیرید.

کد زیر نشان می‌دهد چگونه باید ارجاع به FavoriteColor را حذف کنید:

java
//Before code
public Customer findByCustomerID(Long customerID) {
  stmt = DB.prepare("SELECT CustomerId, FirstName, "+
    "FavoriteColor FROM Customer WHERE CustomerId = ?")
  stmt.setLong(1, customerID.longValue());
  stmt.execute();
  ResultSet rs = stmt.executeQuery();
  if (rs.next()) {
    customer.setCustomerId(rs.getLong("CustomerId"));
    customer.setFirstName(rs.getString("FirstName"));
    customer.setFavoriteColor(rs.getString
    ("FavoriteColor"));
  }
  return customer;
}

public void insert(long customerId,String firstName, String favoriteColor) {
  stmt = DB.prepare("INSERT into customer" +
    "(Customerid, FirstName, FavoriteColor)" +
    "values (?, ?, ?)");
  stmt.setLong(1, customerId);
  stmt.setString(2, firstName);
  stmt.setString(3, favoriteColor);
  stmt.execute();

}

public void update(long customerId, String firstName, String color) {
  stmt = DB.prepare("UPDATE Customer "+
  "SET FirstName = ?, FavoriteColor=? " +
  "WHERE Customerid = ?");
  stmt.setString(1, firstName);
  stmt.setString(2, color);
  stmt.setLong(3, customerId);
  stmt.executeUpdate();
}

//After code
public Customer findByCustomerID(Long customerID) {
  stmt = DB.prepare("SELECT CustomerId, FirstName " +
    "FROM Customer WHERE CustomerId = ?");
  stmt.setLong(1, customerID.longValue());
  stmt.execute();
  ResultSet rs = stmt.executeQuery();
  if (rs.next()) {
    customer.setCustomerId(rs.getLong("CustomerId"));
    customer.setFirstName(rs.getString("FirstName"));
  }
  return customer;
}

public void insert(long customerId,String firstName) {
  stmt = DB.prepare("INSERT into customer" +
    "(Customerid, FirstName) " +
    "values (?, ?)");
  stmt.setLong(1, customerId);
  stmt.setString(2, firstName);
  stmt.execute();
}

public void update(long customerId, String firstName, String color) {
  stmt = DB.prepare("UPDATE Customer "+
  "SET FirstName = ? " +
  "WHERE Customerid = ?");
  stmt.setString(1, firstName);
  stmt.setLong(2, customerId);
  stmt.executeUpdate();
}

Drop Table (حذف جدول)

حذف یک جدول موجود از پایگاه داده.

انگیزه

Drop Table را وقتی اعمال کنید که جدول دیگر لازم یا استفاده نمی‌شود. این وقتی رخ می‌دهد که جدول با منبع دادهٔ مشابه دیگری — مانند جدول یا نما — جایگزین شده باشد، یا ساده‌تر اینکه دیگر به آن منبع دادهٔ خاص نیاز نباشد.

مبادلات بالقوه

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

مکانیک‌های به‌روزرسانی طرحواره

برای Drop Table باید مسائل یکپارچگی داده را حل کنید. اگر TaxJurisdictions توسط جداول دیگر ارجاع داده می‌شود، باید محدودیت کلید خارجی را حذف کنید یا آن را به جدول دیگری اشاره دهید. شکل ۶.۲ نمونه‌ای از حذف جدول TaxJurisdictions را نشان می‌دهد — ابتدا جدول را منسوخ علامت می‌زنید و پس از تاریخ گذار حذف می‌کنید. کد زیر DDL حذف جدول را نشان می‌دهد:

شکل ۶.۲. حذف جدول TaxJurisdictions.

sql
-- drop date = June 14 2007
DROP TABLE TaxJurisdictions;

می‌توانید فقط جدول را تغییر نام دهید، همان‌طور که پایین نشان داده شده. در این کار، برخی محصولات پایگاه داده همهٔ ارجاع‌ها را از TaxJurisdictions به TaxJurisdictionsRemoved به‌طور خودکار تغییر می‌دهند. می‌خواهید آن محدودیت‌های یکپارچگی ارجاعی را با Drop Foreign Key (حذف کلید خارجی) حذف کنید چون شاید نخواهید محدودیت RI به جدولی که قرار است حذف شود داشته باشید:

sql
-- rename date = June 14 2007
ALTER TABLE TaxJurisdictions RENAME TO
TaxJurisdictionsRemoved;

مکانیک‌های مهاجرت داده

تنها مسئلهٔ مهاجرت داده در این بازآرایی، نیاز بالقوه به آرشیو دادهٔ موجود برای بازیابی بعدی است. می‌توانید با دستور CREATE TABLE AS SELECT این کار را انجام دهید. کد زیر DDL حفظ اختیاری داده در جدول TaxJurisdictions را نشان می‌دهد:

sql
-- copy data before drop
CREATE TABLE TaxJurisdictionsRemoved AS
          SELECT * FROM TaxJurisdictions;

-- drop date = June 14 2007
DROP TABLE TaxJurisdictions;

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

هر برنامهٔ بیرونی که به TaxJurisdictions ارجاع می‌دهد باید بازآرایی شود تا به منبع(های) دادهٔ جایگزین دسترسی پیدا کند. اگر جایگزینی نیست و داده هنوز لازم است، نباید جدول را تا وجود جایگزین(ها) حذف کنید.

Drop View (حذف نما)

حذف یک نمای موجود.

انگیزه

Drop View را وقتی اعمال کنید که نما دیگر لازم یا استفاده نمی‌شود. این وقتی رخ می‌دهد که نما با منبع دادهٔ مشابه دیگری — مانند نمای دیگر یا جدول — جایگزین شده باشد، یا ساده‌تر اینکه دیگر به آن پرس‌وجوی خاص نیاز نباشد.

مبادلات بالقوه

حذف نما هیچ داده‌ای را از پایگاه داده پاک نمی‌کند؛ اما یعنی نما دیگر برای برنامه‌های بیرونی که به آن دسترسی دارند در دسترس نیست. نماها اغلب برای گزارش‌ها استفاده می‌شوند. اگر داده هنوز لازم است، نما باید از قبل با منبع دادهٔ دیگری — نما، جدول یا پرس‌وجوی مستقیم به دادهٔ مبدأ — جایگزین شده باشد. این رویکرد جدید دسترسی به داده ایده‌آل باید به‌اندازه یا بهتر از نمایی که حذف می‌شود عمل کند. نماها همچنین برای کنترل دسترسی امنیتی (Security Access Control — SAC) به مقادیر داده در پایگاه داده به‌کار می‌روند. در این حالت، استراتژی SAC جدید برای جداولی که نما به آن‌ها دسترسی دارد باید از قبل پیاده‌سازی و مستقر شده باشد. استراتژی امنیتی مبتنی بر نما اغلب رویکرد «کمترین مشترک» است که بین برنامه‌های زیاد به‌اشتراک گذاشته می‌شود اما به‌اندازهٔ استراتژی SAC برنامه‌نویسی انعطاف‌پذیر نیست (Ambler 2003).

مکانیک‌های به‌روزرسانی طرحواره

برای حذف نمای شکل ۶.۳ باید پس از دورهٔ گذار دستور DROP VIEW را روی AccountDetails اعمال کنید. کد حذف نمای AccountDetails بسیار ساده است — فقط نما را منسوخ علامت می‌زنید و پس از تاریخ گذار حذف می‌کنید.

شکل ۶.۳. حذف نمای AccountDetails.

sql
-- drop date = June 14 2007
DROP VIEW AccountDetails;

مکانیک‌های مهاجرت داده

برای این بازآرایی پایگاه داده داده‌ای برای مهاجرت نیست.

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

باید هر برنامهٔ بیرونی که به AccountDetails ارجاع می‌دهد را شناسایی و به‌روز کنید. شاید لازم باشد کد SQL که قبلاً از AccountDetails استفاده می‌کرد را بازآرایی کنید تا مستقیماً از جداول مبدأ داده بخواند. به‌همین شکل، هر متادیتایی که برای تولید SQL با AccountDetails به‌کار می‌رفت باید به‌روز شود. کد زیر نشان می‌دهد چگونه باید کد برنامه را تغییر دهید تا از جداول پایه داده بگیرد:

java
// Before code
stmt.prepare(
  "SELECT * " +
  "FROM AccountDetails "+
  "WHERE CustomerId = ?");
  stmt.setLong(1,customer.getCustomerID);
  stmt.execute();
  ResultSet rs = stmt.executeQuery();

// After code
stmt.prepare(
"SELECT * " +
"FROM Customer, Account " +
"WHERE" +
" Customer.CustomerId = Account.CustomerId " +
" AND Customer.CustomerId = ?");

stmt.setLong(1,customer.getCustomerID);
stmt.execute();
ResultSet rs = stmt.executeQuery();

Introduce Calculated Column (معرفی ستون محاسباتی)

معرفی ستون جدیدی بر اساس محاسبات روی داده در یک یا چند جدول. (شکل ۶.۴ دو جدول را نشان می‌دهد، اما می‌تواند هر تعداد باشد.)

شکل ۶.۴. معرفی ستون محاسباتی Customer.TotalAccountBalance.

انگیزه

دلیل اصلی اعمال Introduce Calculated Column بهبود عملکرد برنامه با ارائهٔ مقادیر ازپیش‌محاسبه‌شده برای ویژگی‌ای است که از دادهٔ دیگر مشتق می‌شود. مثلاً ممکن است بخواهید ستون محاسباتی معرفی کنید که سطح ریسک اعتباری مشتری (مثلاً نمونه، ریسک خوب، ریسک بد) را بر اساس سابقهٔ پرداخت قبلی او با شرکت شما نشان دهد.

مبادلات بالقوه

ستون محاسباتی ممکن است با مقادیر واقعی داده همگام نماند، به‌ویژه وقتی برنامه‌های بیرونی باید مقدار را به‌روز کنند. پیشنهاد می‌کنیم مکانیزمی — مانند job دسته‌ای منظم یا trigger روی دادهٔ مبدأ — معرفی کنید که ستون محاسباتی را خودکار به‌روز کند.

مکانیک‌های به‌روزرسانی طرحواره

اعمال Introduce Calculated Column می‌تواند به‌خاطر وابستگی‌های داده و نیاز به همگام نگه‌داشتن ستون محاسباتی با مقادیری که بر آن استوار است پیچیده باشد. باید کارهای زیر را انجام دهید:

  1. استراتژی همگام‌سازی را تعیین کنید. انتخاب‌های اصلی شما job دسته‌ای، به‌روزرسانی برنامه یا trigger پایگاه داده است. job دسته‌ای وقتی مناسب است که به‌روزرسانی بلادرنگ لازم نیست؛ وگرنه باید یکی از دو استراتژی دیگر را به‌کار ببرید. وقتی برنامه(ها) مسئول به‌روزرسانی‌های مناسب هستند، خطر این را دارید که برنامه‌های مختلف آن را به روش‌های مختلف انجام دهند. رویکرد trigger احتمالاً از دو استراتژی بلادرنگ امن‌تر است چون منطق فقط یک‌بار در پایگاه داده پیاده می‌شود. شکل ۶.۴ فرض استفاده از trigger را دارد.

  2. نحوهٔ محاسبهٔ مقدار را تعیین کنید. باید دادهٔ مبدأ و نحوهٔ استفاده از آن برای تعیین مقدار TotalAccountBalance را شناسایی کنید.

  3. جدول حاوی ستون را تعیین کنید. باید مشخص کنید کدام جدول TotalAccountBalance را شامل شود. از خود بپرسید ستون محاسباتی بهترین توصیف کدام موجودیت کسب‌وکار است. مثلاً شاخص ریسک اعتباری مشتری بیشتر به موجودیت Customer مربوط است.

  4. ستون جدید را اضافه کنید. Customer.TotalAccountBalance شکل ۶.۴ را از طریق transformation Introduce New Column (معرفی ستون جدید) اضافه کنید.

  5. استراتژی به‌روزرسانی را پیاده‌سازی کنید. باید استراتژی انتخاب‌شده در گام ۱ را پیاده‌سازی و تست کنید.

کد زیر نشان می‌دهد چگونه ستون Customer.TotalAccountBalance و trigger UpdateCustomerTotalAccountBalance را اضافه کنید — trigger هر بار جدول Account تغییر کند اجرا می‌شود:

sql
-- Create the new column TotalAccountBalance
ALTER TABLE Customer ADD TotalAccountBalance NUMBER;

-- Create trigger to keep data in sync.

CREATE OR REPLACE TRIGGER
UpdateCustomerTotalAccountBalance
BEFORE UPDATE OR INSERT OR DELETE
ON Account
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
NewBalanceToUpdate NUMBER:=0;
CustomerIdToUpdate NUMBER;
BEGIN
CustomerIdToUpdate := :NEW.CustomerID;
IF UPDATING THEN
  NewBalanceToUpdate := :NEW.Balance-:OLD.Balance;
END IF;
IF INSERTING THEN
  NewBalanceToUpdate := :NEW.Balance;
END IF;
IF DELETING THEN
  NewBalanceToUpdate := -1*:OLD.Balance;
  CustomerIdToUpdate := :OLD.CustomerID;
END IF;
UPDATE Customer SET TotalAccountBalance =
  TotalAccountBalance + NewBalanceToUpdate
  WHERE Customerid = CustomerIdToUpdate;
END;
/

مکانیک‌های مهاجرت داده

به‌معنای دقیق داده‌ای برای مهاجرت نیست، اگرچه مقدار Customer.TotalAccountBalance باید بر اساس محاسبه پر شود. این معمولاً یک‌بار با دستور UPDATE SQL یا در دسته با یک یا چند اسکript انجام می‌شود. کد زیر نشان می‌دهد چگونه مقدار اولیه در Customer.TotalAccountBalance تنظیم شود:

sql
UPDATE Customer SET
  TotalAccountBalance =
    (SELECT SUM(balance) FROM Account
      WHERE Account.CustomerId = Customer.CustomerId)

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

وقتی ستون محاسباتی را معرفی می‌کنید، باید همهٔ جاهایی در برنامه‌های بیرونی که این محاسبه انجام می‌شود را شناسایی و کد را برای کار با TotalAccountBalance بازکار کنید. باید منطق محاسبهٔ موجود را با دسترسی به مقدار TotalAccountBalance جایگزین کنید. ممکن است ببینید محاسبه در برنامه‌های مختلف به‌شکل متفاوت انجام می‌شود — یا به‌خاطر باگ یا موقعیت متفاوت — و بنابراین باید الگوریتم درست را با ذی‌نفع(ان) هماهنگ کنید. کد زیر نشان می‌دهد چگونه برنامه برای محاسبهٔ مجموع موجودی با پیمایش همهٔ حساب‌های مشتری به‌کار می‌رود. در «نسخهٔ بعد»، هنگام بازیابی شیء customer از پایگاه داده، مقدار را ساده می‌خواند:

java
// Before code
stmt.prepare(
  "SELECT SUM(Account.Balance) Balance FROM Customer, Account " +
    "WHERE Customer.CustomerID = Account.CustomerID "+
    "AND Customer.CustomerID=?");
stmt.setLong(1,customer.getCustomerID);
stmt.execute();
ResultSet rs = stmt.executeQuery();
return rs.getBigDecimal("Balance"));

//After code
return customer.getBalance();

Introduce Surrogate Key (معرفی کلید جایگزین)

جایگزینی کلید طبیعی موجود با کلید جایگزین. این بازآرایی مخالف Replace Surrogate Key With Natural Key (جایگزینی کلید جایگزین با کلید طبیعی) است.

انگیزه

چند دلیل برای معرفی کلید جایگزین به جدول وجود دارد:

  • کاهش coupling. دلیل اصلی کاهش coupling بین طرحوارهٔ جدول و دامنهٔ کسب‌وکار است. اگر بخشی از کلید طبیعی احتمال تغییر دارد — مثلاً اگر شمارهٔ قطعه در جدول موجودی احتمالاً بزرگ‌تر شود یا نوعش عوض شود (از عددی به الفبایی-عددی) — داشتن آن به‌عنوان کلید اصلی خطرناک است.

  • افزایش یکنواختی. ممکن است بخواهید بازآرایی Consolidate Key Strategy (یکپارچه‌سازی استراتژی کلید) را اعمال کنید و احتمالاً عملکرد را بهبود دهید و پیچیدگی کد را کم کنید.

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

مبادلات بالقوه

بسیاری از متخصصان داده کلیدهای طبیعی را ترجیح می‌دهند. بحث کلید جایگزین در برابر طبیعی در جامعهٔ داده «مسئلهٔ مذهبی» است، اما واقعیت این است که هر دو نوع کلید جای خود را دارند. حتی اگر جدول کلید اصلی جایگزین داشته باشد، ممکن است هنوز به کلیدهای جایگزین طبیعی برای جستجو نیاز داشته باشید. چون کلید جایگزین معنای کسب‌وکار ندارد و معمولاً به‌صورت مجموعه‌ای از کاراکترها یا اعداد بی‌معنا پیاده می‌شود، کاربران نهایی نمی‌توانند با آن جستجو کنند. در نتیجه هنوز باید داده را با شناسه‌های طبیعی پیدا کنند. مثلاً جدول InventoryItem کلید اصلی جایگزین InventoryItemPOID (POID مخفف persistent object identifier) و کلید جایگزین طبیعی InventoryID دارد. اقلام در سیستم با کلید جایگزین یکتا شناسایی می‌شوند اما کاربران با کلید طبیعی. ارزش کلید جایگزین ساده‌تر کردن استراتژی کلید در پایگاه داده و کاهش coupling بین طرحواره و دامنهٔ کسب‌وکار است.

چالش دیگر این است که ممکن است کلید جایگزین را وقتی واقعاً لازم نیست پیاده کنید. بسیاری در پیاده‌سازی کلیدها زیاده‌رو می‌کنند و همان استراتژی را در کل طرحواره اعمال می‌کنند. مثلاً در ایالات متحده، ایالت‌ها با کد دوحرفی یکتا شناسایی می‌شوند (مثلاً CA برای کالیفرنیا). این کد در ایالات متحده و کانادا یکتا است؛ کد استان Ontario در کانادا ON است و هرگز ایالت آمریکایی با آن کد نخواهد بود. ایالت‌ها و استان‌ها موجودیت‌های نسبتاً پایدارند، تعداد زیادی کد هنوز آزاد است (فقط ۶۵ از ۶۷۶ ترکیب ممکن تا امروز استفاده شده) و به‌خاطر اختلالی که در سیستم‌های خودشان ایجاد می‌کند، دولت‌ها احتمالاً استراتژی را عوض نمی‌کنند. پس آیا واقعاً معرفی کلید جایگزین به جدول lookup همهٔ ایالت‌ها و استان‌ها منطقی است؟ احتمالاً نه.

همچنین وقتی OriginalKey در جداول دیگر به‌عنوان کلید خارجی استفاده می‌شود، باید Consolidate Key Strategy را اعمال و به‌روزرسانی‌های مشابه را در آن جداول انجام دهید. توجه کنید این ممکن است بیش از ارزشش کار باشد؛ شاید بخواهید از اعمال این بازآرایی صرف‌نظر کنید.

مکانیک‌های به‌روزرسانی طرحواره

اعمال Introduce Surrogate Key می‌تواند به‌خاطر coupling که کلید اصلی — در مثال ما ترکیب CustomerNumber، OrderDate و StoreID — احتمالاً دارد پیچیده باشد. چون کلید اصلی جدول است، احتمالاً (بخشی از) کلید خارجی برگشت به Order از جداول دیگر هم هست. باید کارهای زیر را انجام دهید:

  1. ستون کلید جدید را معرفی کنید. ستون را با دستور SQL ADD COLUMN به جدول هدف اضافه کنید. در شکل ۶.۵ این OrderPOID است. این ستون باید با مقادیر یکتا پر شود.

شکل ۶.۵. معرفی کلید جایگزین Order.OrderPOID.

  1. index جدید اضافه کنید. index جدید بر اساس OrderPOID برای Order باید معرفی شود.

  2. ستون اصلی را منسوخ کنید. ستون‌های کلید اصلی باید برای تنزل به وضعیت کلید جایگزین یا غیرکلید در پایان دورهٔ گذار علامت بخورند. در مثال ما، ستون‌ها در این زمان از Order حذف نمی‌شوند؛ فقط دیگر کلید اصلی محسوب نمی‌شوند. از OrderItem حذف می‌شوند.

  3. triggerهای یکپارچگی ارجاعی (RI) را به‌روز و در صورت لزوم اضافه کنید. triggerهای موجود برای حفظ RI بین جداول باید با مقادیر کلید جدید متناظر در جداول دیگر کار کنند. triggerهایی برای پر کردن ستون‌های کلید خارجی در دورهٔ گذار لازم است چون برنامه‌ها شاید هنوز به‌روز نشده باشند.

شکل ۶.۵ نشان می‌دهد چگونه OrderPOID، کلید جایگزین، به جدول Order معرفی شود. همچنین باید به‌صورت بازگشتی Introduce Surrogate Key را روی جدول OrderItem شکل ۶.۵ اعمال کنید تا از ستون کلید جدید استفاده شود. این اختیاری است. البته OrderItem می‌تواند هنوز از کلید composite موجود از CustomerNumber، OrderDate و StoreID استفاده کند، اما برای یکنواختی تصمیم گرفتیم آن جدول را هم بازآرایی کنیم.

کد SQL زیر ستون OrderPOID را در جداول Order و OrderItem معرفی و اولیه پر می‌کند. مقادیر یکتا برای OrderPOID با فراخوانی stored procedure GenerateUniqueID به‌دست می‌آید که الگوریتم HIGH-LOW را پیاده می‌کند (Ambler 2003). index مناسب برای پشتیبانی از OrderPOID به‌عنوان کلید Order هم معرفی می‌شود:

sql
-- Add new surrogate key to Order table
ALTER TABLE Order ADD OrderPOID NUMBER;

-- Add new surrogate foreign key to OrderItem table
ALTER TABLE OrderItem ADD OrderPOID NUMBER;

-- Assign values to surrogate key column in Order
UPDATE Order SET OrderPOID =
getOrderPOIDFromOrder(CustomerNumber,OrderDate,StoreID);

-- Propagate ForeignKey in OrderItem
UPDATE OrderItem SET OrderPOID =
  (SELECT OrderPOID FROM Order
    WHERE CustomerNumber = Order.CustomerNumber
      AND OrderDate = Order.OrderDate
      AND StoreID=Order.StoreID);

CREATE INDEX OrderOrderPOIDIndex ON Order (OrderPOID);

برای پشتیبانی از این کلید جدید، trigger PopulateOrderPOID را اضافه می‌کنیم که هر درج در OrderItem رخ دهد فراخوانی می‌شود. این trigger مقدار Order.OrderPOID را به‌دست می‌آورد، همان‌طور که در کد SQL زیر می‌بینید:

sql
CREATE OR REPLACE TRIGGER PopulateOrderPOID
BEFORE INSERT
ON OrderItem
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
IF :NEW.OrderPOID IS NULL THEN
  :NEW.OrderPOID :=
getOrderPOIDFromOrder(CustomerNumber,OrderDate,StoreID);
END IF;
IF :NEW.OrderPOID IS NOT NULL THEN
  IF :NEW.CustomerNumber IS NULL
     OR :NEW.OrderDate IS NULL
     OR :NEW.StoreID IS NULL
  THEN
     :NEW.CustomerNumber
       := getCustomerNumberFromOrder(OrderPOID);
       :NEW.OrderDate
:= getOrderDateFromOrder(OrderPOID);
  :NEW.StoreID
:= getStoreIDFromOrder(OrderPOID);
  END IF;
END IF;
END;
/

-- June 14 2007
ALTER TABLE OrderItem DROP CONSTRAINT
OrderItemToOrderForeignKey;

ALTER TABLE Order DROP CONSTRAINT

OrderPrimaryKey;

ALTER TABLE Order MODIFY OrderPOID NOT NULL;

ALTER TABLE Order ADD CONSTRAINT OrderPrimaryKey
  PRIMARY KEY (OrderPOID);

ALTER TABLE OrderItem DROP CONSTRAINT
OrderItemPrimaryKey;

ALTER TABLE OrderItem MODIFY OrderPOID NOT NULL;
ALTER TABLE OrderItem ADD CONSTRAINT OrderItemPrimaryKey
  PRIMARY KEY (OrderPOID, OrderItemNumber);

ALTER TABLE OrderItem ADD (CONSTRAINT
OrderItemToOrderForeignKey
  FOREIGN KEY (OrderPOID) REFERENCES Order;

CREATE UNIQUE INDEX OrderNaturalKey ON Order
(CustomerNumber,OrderDate,StoreID);

DROP TRIGGER PopulateOrderPOID;

مکانیک‌های مهاجرت داده

باید داده در Order.OrderPOID تولید شود و این مقادیر به ستون‌های کلید خارجی در جداول دیگر اختصاص یابد.

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

هر برنامهٔ بیرونی که به ستون‌های کلید اصلی ارجاع می‌دهد باید برای کار با Order.OrderPOID به‌روز شود. شاید لازم باشد کد را برای موارد زیر بازکار کنید:

  1. اختصاص مقادیر کلید جدید. اگر کد برنامهٔ بیرونی مقادیر کلید جایگزین جدید را اختصاص می‌دهد — به‌جای خود پایگاه داده — همهٔ برنامه‌های بیرونی باید برای اختصاص مقدار به Order.OrderPOID بازکار شوند. حداقل هر برنامه باید همان الگوریتم را پیاده کند، اما استراتژی بهتر سرویس مشترکی است که همهٔ برنامه‌ها فراخوانی کنند.

  2. join بر اساس کلید جدید. بسیاری از برنامه‌های دسترسی بیرونی joinهایی شامل Order تعریف می‌کنند — یا با SQL سخت‌کد شده یا با متادیتا. این joinها باید با Order.OrderPOID بازآرایی شوند.

  3. بازیابی بر اساس کلید جدید. برخی برنامه‌ها پایگاه داده را سطر به سطر پیمایش می‌کنند و بر اساس مقادیر کلید داده بازیابی می‌کنند. این بازیابی‌ها باید با Order.OrderPOID به‌روز شوند.

نگاشت Hibernate زیر نشان می‌دهد کلید جایگزین چگونه معرفی می‌شود:

xml
//Before mapping
<hibernate-mapping>
  <class name="Order" table="ORDER">
    <many-to-one name="customer"
    class="Customer" column="CUSTOMERNUMBER" />
    <property name="orderDate"/>
    <property name="storeID"/>

    <property name="shipTo"/>
    <property name="billTo"/>
    <property name="total"/>
  </class>
</hibernate-mapping>
//After mapping
<hibernate-mapping>
  <class name="Order" table="ORDER">
    <id name="id" column="ORDERPOID">
      <generator class="OrderPOIDGenerator"/>
    </id>
    <many-to-one name="customer"
     class="Customer" column="CUSTOMERNUMBER" />
    <property name="orderDate"/>
    <property name="storeID"/>
    <property name="shipTo"/>
    <property name="billTo"/>
    <property name="total"/>
  </class>
</hibernate-mapping>

Merge Columns (ادغام ستون‌ها)

ادغام دو یا چند ستون در یک جدول.

انگیزه

چند دلیل برای اعمال Merge Columns وجود دارد:

  • ستون یکسان. دو یا چند توسعه‌دهنده ممکن است بدون اطلاع یکدیگر ستون‌ها را اضافه کرده باشند — اتفاق رایج وقتی در تیم‌های مختلف هستند یا متادیتای توصیف‌کنندهٔ طرحواره جدول در دسترس نیست. مثلاً جدول FeeStructure ۳۷ ستون دارد که ۲ تای آن CA_IN و CheckingAccountOpeningFee نام دارند و هر دو کارمزد اولیهٔ افتتاح حساب جاری را ذخیره می‌کنند. ستون دوم اضافه شد چون کسی مطمئن نبود CA_INIT واقعاً برای چه استفاده می‌شود.

  • ستون‌ها نتیجهٔ طراحی بیش‌ازحد هستند. ستون‌های اصلی برای ذخیرهٔ اطلاعات به‌صورت اجزای سازنده معرفی شدند، اما استفادهٔ واقعی نشان می‌دهد به جزئیات دقیقی که فکر می‌کردید نیاز ندارید. مثلاً جدول Customer شکل ۶.۶ شامل ستون‌های PhoneCountryCode، PhoneAreaCode و PhoneLocal برای نمایش یک شماره تلفن است.

شکل ۶.۶. ادغام ستون‌ها در جدول Customer.

  • استفاده از ستون‌ها یکسان شده. چند ستون در ابتدا به جدول اضافه شدند، اما با گذشت زمان نحوهٔ استفاده از یکی یا چندتای آن‌ها به‌جایی رسیده که برای همان هدف به‌کار می‌روند. مثلاً جدول Customer شامل PreferredCheckStyle و SelectedCheckStyle است (در شکل ۶.۶ نشان داده نشده). ستون اول برای ثبت سبک چکی که فصل بعد به مشتری ارسال شود استفاده می‌شد و دوم برای سبکی که قبلاً برایش ارسال شده بود — ۲۰ سال پیش مفید بود چون سفارش چک جدید چند ماه طول می‌کشید، اما اکنون که یک‌شبه چاپ می‌شوند، همان مقدار را خودکار در هر دو ستون ذخیره می‌کنیم.

مبادلات بالقوه

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

مکانیک‌های به‌روزرسانی طرحواره

برای Merge Columns دو کار لازم است. اول، ستون جدید را معرفی کنید. ستون را با دستور SQL ADD COLUMN به جدول اضافه کنید. در شکل ۶.۶ این Customer.PhoneNumber است. این گام اختیاری است چون شاید بتوانید از یکی از ستون‌های موجود برای ادغام داده استفاده کنید. همچنین trigger همگام‌سازی لازم است تا ستون‌ها با یکدیگر همگام بمانند. trigger باید با هر تغییر در ستون‌ها فراخوانی شود.

شکل ۶.۶ مثالی است که جدول Customer در ابتدا شماره تلفن را در سه ستون جدا ذخیره می‌کند: PhoneCountryCode، PhoneAreaCode و PhoneLocal. با گذشت زمان فهمیدیم برنامه‌های کمی به کد کشور علاقه‌مندند چون فقط در آمریکای شمالی استفاده می‌شوند. همچنین هر برنامه area code و شماره محلی را با هم استفاده می‌کند. بنابراین PhoneCountryCode را دست‌نخورده گذاشتیم اما PhoneAreaCode و PhoneLocal را در PhoneNumber ادغام کردیم — منعکس‌کنندهٔ استفادهٔ واقعی برنامه (چون برنامه PhoneAreaCode یا PhoneLocal را جداگانه استفاده نمی‌کند). trigger SynchronizePhoneNumber را برای همگام نگه‌داشتن مقادیر چهار ستون معرفی کردیم.

کد SQL زیر DDL معرفی ستون PhoneNumber و در نهایت حذف دو ستون اصلی را نشان می‌دهد:

sql
ALTER TABLE Customer ADD PhoneNumber NUMBER(12);
          COMMENT ON Customer.PhoneNumber 'Added as the
          result of merging Customer.PhoneAreaCode and
          Customer.PhoneLocal finaldate = December 14 2007';

-- On December 14 2007
ALTER TABLE Customer DROP COLUMN PhoneAreaCode;
ALTER TABLE Customer DROP COLUMN PhoneLocal;

مکانیک‌های مهاجرت داده

باید همهٔ داده از ستون(های) اصلی به ستون ادغام‌شده تبدیل شود — در این مورد از Customer.PhoneAreaCode و Customer.PhoneLocal به Customer.PhoneNumber. کد SQL زیر DML ترکیب اولیهٔ داده از PhoneAreaCode و PhoneLocal در PhoneNumber را نشان می‌دهد:

sql
/*One-time migration of data from Customer.PhoneAreaCode and Customer.PhoneLocal to
Customer.PhoneNumber. When both the columns are active, there is a need to have a trigger that
keeps both the columns in sync */
UPDATE Customer SET PhoneNumber = PhoneAreaCode*10000000 + PhoneLocal;

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

باید برنامه‌های دسترسی را کامل بررسی و در دورهٔ گذار به‌روز کنید. علاوه بر واضح‌ترین مورد — کار با Customer.PhoneNumber به‌جای ستون‌های ادغام‌نشده قبلی — موارد زیر را در نظر بگیرید:

حذف کد ادغام. ممکن است کدی باشد که ستون‌های موجود را به ویژگی داده‌ای مشابه ستون ادغام‌شده ترکیب می‌کند. این کد باید بازآرایی و احتمالاً کاملاً حذف شود.

به‌روزرسانی کد اعتبارسنجی داده برای کار با دادهٔ ادغام‌شده. برخی کدهای اعتبارسنجی فقط به‌خاطر ادغام‌نشده بودن ستون‌ها وجود دارند. مثلاً اگر مقدار در دو ستون جدا ذخیره شده، کد اعتبارسنجی ممکن است یکسان بودن مقادیر را بررسی کند. پس از ادغام، شاید دیگر به این کد نیاز نباشد.

قطعهٔ کد قبل و بعد نشان می‌دهد متد getCustomerPhoneNumber() هنگام ادغام Customer.PhoneAreaCode و Customer.PhoneLocal چگونه تغییر می‌کند:

java
//Before code
public String getCustomerPhoneNumber(Customer customer){
  String phoneNumber = customer.getCountryCode();
  phoneNumber.concat(phoneNumberDelimiter());
  phoneNumber.concat(customer.getPhoneAreaCode());
  phoneNumber.concat(customer.getPhoneLocal());
  return phoneNumber;
}

//After code
public String getCustomerPhoneNumber(Customer customer){
  String phoneNumber = customer.getCountryCode();
  phoneNumber.concat(phoneNumberDelimiter());
  phoneNumber.concat(customer.getPhoneNumber());
  return phoneNumber;
}

Merge Tables (ادغام جداول)

ادغام دو یا چند جدول در یک جدول.

انگیزه

چند دلیل برای اعمال Merge Tables وجود دارد:

  • جداول نتیجهٔ طراحی بیش‌ازحد هستند. جداول اصلی برای ذخیرهٔ اطلاعات به‌صورت اجزای سازنده معرفی شدند، اما استفادهٔ واقعی نشان می‌دهد به جزئیات دقیقی که فکر می‌کردید نیاز ندارید. مثلاً جدول Employee ستون‌های شناسایی کارمند و داده‌های دیگر را دارد، در حالی که جدول EmployeeIdentification فقط اطلاعات شناسایی را ثبت می‌کند.

  • استفاده از جداول یکسان شده. با گذشت زمان نحوهٔ استفاده از یک یا چند جدول به‌جایی رسیده که چند جدول برای همان هدف به‌کار می‌روند. همچنین ممکن است جداولی داشته باشید که به‌صورت یک-به-یک مرتبط‌اند؛ شاید بخواهید جداول را ادغام کنید تا نیاز به join با جدول دیگر نباشد. مثال خوب همان جدول Employee است که در ابتدا برای ثبت اطلاعات کارمند بود، اما EmployeeIdentification برای ذخیرهٔ فقط اطلاعات شناسایی معرفی شد. برخی نمی‌دانستند این جدول وجود دارد و جدول Employee را برای ثبت دادهٔ مشابه تکامل دادند.

  • جدول اشتباه تکرار شده. دو یا چند توسعه‌دهنده ممکن است بدون اطلاع یکدیگر جداول را اضافه کرده باشند — اتفاق رایج وقتی در تیم‌های مختلف هستند یا متادیتای توصیف‌کنندهٔ طرحواره در دسترس نیست. مثلاً جداول FeeStructure و FeeSchedule هر دو کارمزد اولیهٔ افتتاح حساب جاری را ذخیره می‌کنند. جدول دوم اضافه شد چون کسی مطمئن نبود FeeStructure واقعاً برای چه استفاده می‌شود.

مبادلات بالقوه

ادغام دو یا چند جدول می‌تواند هنگام ادغام جداول با جزئیات دقیق منجر به از دست رفتن دقت داده شود. وقتی جداولی را که (فکر می‌کنید) برای همان هدف استفاده می‌شوند ادغام می‌کنید، خطر این را دارید که در واقع باید جداگانه به‌کار بروند. مثلاً EmployeeIdentification ممکن است برای جدا کردن اطلاعات حساس امنیتی در جدولی با حقوق دسترسی محدود معرفی شده باشد. در آن صورت متوجه می‌شوید باید یکی یا چند جدول اصلی را دوباره معرفی کنید. استفاده از داده باید تعیین کند آیا جداول باید ادغام شوند.

مکانیک‌های به‌روزرسانی طرحواره

همان‌طور که در شکل ۶.۷ نشان داده شده، برای به‌روزرسانی طرحواره هنگام Merge Tables دو کار لازم است. اول، جدول ادغام‌شده را با افزودن ستون‌های EmployeeIdentification به جدول Employee از طریق دستور SQL ADD COLUMN معرفی کنید. توجه کنید Employee ممکن است از قبل برخی یا همهٔ ستون‌های لازم را داشته باشد — در آن صورت ناسازگاری یا کد دامنهٔ شلوغ وجود دارد؛ این بازآرایی باید ساده‌سازی کد برنامه را ممکن کند.

شکل ۶.۷. انتقال همهٔ ستون‌ها از EmployeeIdentification به Employee.

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

شکل ۶.۷ مثالی است که جدول Employee در ابتدا دادهٔ کارمند را ذخیره می‌کند. با گذشت زمان جدول EmployeeIdentification هم برای اطلاعات شناسایی اضافه شد. بنابراین Employee و EmployeeIdentification را ادغام کردیم تا همهٔ اطلاعات کارمند در یک جا باشد. trigger SynchronizeIdentification را برای همگام نگه‌داشتن مقادیر در جداول معرفی کردیم. کد SQL زیر DDL معرفی ستون‌های Picture، VoicePrint، RetinalPrint و در نهایت حذف جدول EmployeeIdentification را نشان می‌دهد:

sql
ALTER TABLE Employee ADD Picture BINARY;

COMMENT ON Employee.Picture 'Added as the result of merging Employee and
 EmployeeIdentification
finaldate = December 14 2007';

ALTER   TABLE Employee ADD VoicePrint BINARY;

COMMENT ON Employee.VoicePrint 'Added as the result of merging Employee and
EmployeeIdentification finaldate = December 14 2007';

ALTER TABLE Employee ADD RetinalPrint BINARY;

COMMENT ON Employee.RetinalPrint 'Added as the result of merging Employee and
EmployeeIdentification finaldate = December 14 2007';

مکانیک‌های مهاجرت داده

باید همهٔ داده از جدول(های) اصلی به جدول ادغام‌شده کپی شود — در این مورد از EmployeeIdentification به Employee. این را می‌توان با چند روش انجام داد — مثلاً اسکript SQL یا ابزار ETL. (در این بازآرایی نباید گام transform باشد.)

کد SQL زیر DDL ترکیب اولیهٔ داده از جداول Employee و EmployeeIdentification را نشان می‌دهد:

sql
/*One-time migration of data from Employee to
EmployeeIdentification. When both the tables are active, there is a need to have a trigger that
 keeps both the tables in sync
          */

           UPDATE Employee e SET e.Picture =
             (SELECT ei.Picture FROM
             EmployeeIdentificaion ei
               WHERE
                 ei.EmployeeNumber = e.EmployeeNumber);

           UPDATE Employee e SET e.VoicePrint =
             (SELECT ei.VoicePrint FROM
             EmployeeIdentificaion ei
               WHERE
                 ei.EmployeeNumber = e.EmployeeNumber);

           UPDATE Employee e SET e.RetinalPrint =
             (SELECT ei.RetinalPrint FROM
             EmployeeIdentificaion ei
               WHERE
       ei.EmployeeNumber = e.EmployeeNumber);

-- On December 14 2007
DROP TRIGGER SynchronizeWithEmployee;
DROP TRIGGER SynchronizeWith-
EmployeeIdentification;
DROP TABLE EmployeeIdentification;

کد زیر نشان می‌دهد triggerهای SynchronizeWithEmployeeIdentification و SynchronizeWithEmployee چگونه مقادیر در جداول را همگام نگه می‌دارند:

sql
CREATE TRIGGER SynchronizeWithEmployeeIdentification
          BEFORE INSERT OR UPDATE OR DELETE
          ON Employee
          REFERENCING OLD AS OLD NEW AS NEW
          FOR EACH ROW
          DECLARE
          BEGIN
          IF updating THEN
            updateOrCreateEmployeeIdentification;
          END IF;
          IF inserting THEN
            createNewEmployeeIdentification;
          END IF;
          IF deleting THEN
            deleteEmployeeIdentification;
          END IF;
          END;
          /

            CREATE TRIGGER SynchronizeWithEmployee

           BEFORE INSERT OR UPDATE OR DELETE
           ON EmployeeIdentification
           REFERENCING OLD AS OLD NEW AS NEW
           FOR EACH ROW
           DECLARE
           BEGIN
           IF updating THEN
             updateOrCreateEmployee;
           END IF;
           IF inserting THEN
             createNewEmployee;
           END IF;
           IF deleting THEN
             deleteEmployee;
           END IF;
           END;
/

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

علاوه بر نیاز واضح کار با Employee به‌جای جدول(های) ادغام‌نشدهٔ قبلی، به‌روزرسانی‌های بالقوه:

  1. ساده‌سازی کد دسترسی به داده. برخی کدهای دسترسی ممکن است دو یا چند جدول درگیر ادغام را بخوانند. مثلاً کلاس Employee ممکن است اطلاعاتش را در دو جدولی که اکنون ادغام شده‌اند به‌روز کند.

  2. به‌روزرسانی‌های ناقص یا متناقض. اکنون که داده در یک جا ذخیره می‌شود، ممکن است ببینید برنامه‌های دسترسی فقط با زیرمجموعه‌ای از داده کار می‌کردند. مثلاً کلاس Customer شماره تلفن منزل را در دو جدول به‌روز می‌کند، در حالی که واقعاً در سه جدول ذخیره شده بود (که اکنون ادغام شده). برنامه‌های دیگر ممکن است کیفیت داده در جدول سوم را ضعیف بدانند و کدی برای جبران داشته باشند. مثلاً کلاس گزارش‌گیری NULL را به «Unknown» تبدیل می‌کند، اما اکنون NULL وجود ندارد و این کد قابل حذف است.

  3. برخی دادهٔ ادغام‌شده برای برخی برنامه‌ها لازم نیست. برخی برنامه‌هایی که با Employee کار می‌کنند فقط به دادهٔ خود Employee نیاز دارند. اما اکنون که ستون‌های EmployeeIdentification اضافه شده، ممکن است برنامه‌های موجود این ستون‌های جدید را درست به‌روز نکنند. شاید لازم باشد برنامه‌های موجود برای پذیرش و کار با ستون‌های جدید گسترش یابند. مثلاً جدول مبدأ کلاس Employee ممکن است ستون BirthDate ادغام‌شده داشته باشد. حداقل کلاس Employee نباید این ستون را با دادهٔ نامعتبر بازنویسی کند و هنگام ایجاد customer جدید مقدار مناسب درج کند. شاید لازم باشد Introduce Default Value (معرفی مقدار پیش‌فرض) را روی ستون‌های ادغام‌شده در Employee اعمال کنید.

مثال زیر تغییرات کد هنگام Merge Tables روی Employee و EmployeeIdentification را نشان می‌دهد:

java
//Before code
public Employee getEmployeeInformation (Long
employeeNumber) throws SQLException {
  Employee employee = new Employee();

    stmt.prepare(
    "SELECT EmployeeNumber, Name, PhoneNumber " +
    "FROM Employee" +
    "WHERE EmployeeNumber = ?");
    stmt.setLong(1,employeeNumber);

    stmt.execute();
    ResultSet rs = stmt.executeQuery();
    employee.setEmployeeNumber(rs.getLong
    ("EmployeeNumber"));
    employee.setName(rs.getLong("Name"));
    employee.setPhoneNumber(rs.getLong("PhoneNumber"));

    stmt.prepare(
    "SELECT Picture, VoicePrint, RetinalPrint " +
    "FROM EmployeeIdentification" +
    "WHERE EmployeeNumber = ?");
    stmt.setLong(1,employeeNumber);
    stmt.execute();
    rs = stmt.executeQuery();
    employee.setPicture(rs.getBlob("Picture"));
    employee.setVoicePrint(rs.getBlob("VoicePrint"));
    employee.setRetinalPrint(rs.getBlob("RetinalPrint"));

    return employee;
}

//After code
public Employee getEmployeeInformation (Long
employeeNumber) throws SQLException {
  Employee employee = new Employee();

    stmt.prepare(
    "SELECT EmployeeNumber, Name, PhoneNumber " +
    "Picture, VoicePrint, RetinalPrint "+
    "FROM Employee" +
    "WHERE EmployeeNumber = ?");
    stmt.setLong(1,employeeNumber);
    stmt.execute();
    ResultSet rs = stmt.executeQuery();
    employee.setEmployeeNumber(rs.getLong
    ("EmployeeNumber"));
    employee.setName(rs.getLong("Name"));
    employee.setPhoneNumber(rs.getLong("PhoneNumber"));
    employee.setPicture(rs.getBlob("Picture"));
    employee.setVoicePrint(rs.getBlob("VoicePrint"));
    employee.setRetinalPrint(rs.getBlob("RetinalPrint"));
    return employee;
}

Move Column (انتقال ستون)

انتقال یک ستون جدول، با تمام دادهٔ آن، به جدول موجود دیگر.

انگیزه

چند دلیل برای اعمال Move Column وجود دارد. دو دلیل اول ممکن است متناقض به‌نظر برسند، اما به‌خاطر داشته باشید بازآرایی پایگاه داده موقعیت‌محور است. انگیزه‌های رایج:

  • نرمال‌سازی. رایج است ستون موجود یکی از قواعد نرمال‌سازی را نقض کند. با انتقال ستون به جدول دیگر می‌توانید نرمال‌سازی جدول مبدأ را افزایش دهید و افزونگی داده را کم کنید.

  • Denormalization برای کاهش joinهای رایج. رایج است ببینید جدولی فقط برای دسترسی به یک ستون در join شرکت می‌کند. با انتقال ستون به جدول دیگر می‌توانید عملکرد را با حذف نیاز به این join بهبود دهید.

  • بازسازمان‌دهی جدول تقسیم‌شده. قبلاً Split Table (تقسیم جدول) انجام داده‌اید — یا جدول از ابتدا به‌همین شکل طراحی شده — و متوجه می‌شوید یک ستون دیگر هم باید منتقل شود. شاید ستون در جدول پردسترسی است اما به‌ندرت لازم، یا در جدول کم‌دسترسی است اما اغلب نیاز می‌شود. در حالت اول عملکرد شبکه با عدم انتخاب و ارسال ستون به برنامه‌ها بهتر می‌شود؛ در دوم عملکرد پایگاه داده با joinهای کمتر بهبود می‌یابد.

مبادلات بالقوه

انتقال ستون برای افزایش نرمال‌سازی افزونگی را کم می‌کند اما اگر برنامه‌ها برای دسترسی به داده به join اضافی نیاز داشته باشند ممکن است عملکرد را کاهش دهد. برعکس، اگر با denormalization از طریق انتقال ستون عملکرد را بهبود دهید، افزونگی داده افزایش می‌یابد.

مکانیک‌های به‌روزرسانی طرحواره

برای به‌روزرسانی طرحواره هنگام Move Column باید کارهای زیر را انجام دهید:

  1. قان(های) حذف را شناسایی کنید. وقتی سطری از یک جدول حذف می‌شود چه باید شد؟ سطر متناظر در جدول دیگر حذف شود، مقدار ستون null/صفر شود، مقدار پیش‌فرضی تنظیم شود، یا دست‌نخورده بماند؟ این در کد trigger (بعداً در این بخش) پیاده می‌شود. توجه کنید صفر یا چند trigger حذف ممکن است از قبل برای قواعد RI بین جداول وجود داشته باشد.

  2. قان(های) درج را شناسایی کنید. وقتی سطری در یکی از جداول درج می‌شود چه باید شد؟ سطر متناظر در جدول دیگر درج شود یا هیچ اتفاقی نیفتد؟ این قانون در کد trigger پیاده می‌شود و ممکن است trigger(های) درج از قبل وجود داشته باشد.

  3. ستون جدید را معرفی کنید. ستون را با دستور SQL ADD COLUMN به جدول هدف اضافه کنید. در شکل ۶.۸ این Account.Balance است.

شکل ۶.۸. انتقال ستون Balance از Customer به Account.

  1. trigger معرفی کنید. trigger روی ستون اصلی و جدید لازم است تا در دورهٔ گذار داده را از یک ستون به دیگری کپی کند. این trigger(ها) باید با هر تغییر سطر فراخوانی شوند.

شکل ۶.۸ مثالی است که Customer.Balance به جدول Account منتقل می‌شود. این مسئلهٔ نرمال‌سازی است — به‌جای ذخیرهٔ موجودی هر بار که حساب مشتری به‌روز می‌شود، می‌توانیم آن را یک‌بار برای هر حساب جداگانه ذخیره کنیم. در دورهٔ گذار ستون Balance در Customer و Account ظاهر می‌شود، همان‌طور که انتظار دارید.

triggerهای موجود جالب‌اند. جدول Account از قبل trigger درج و به‌روزرسانی داشت که وجود سطر متناظر در Customer را بررسی می‌کند — بررسی RI پایه. این trigger باقی می‌ماند. جدول Customer trigger حذف داشت تا اگر سطر Account به آن ارجاع دهد حذف نشود — RI دیگر. مزیت این است که برای ستون منتقل‌شده قانون حذف پیاده نکنیم چون نمی‌توانیم «کار اشتباه» کنیم و Customer را حذف کنیم در حالی که Account به آن ارجاع دارد.

در کد زیر ستون Account.Balance و triggerهای SynchronizeCustomerBalance و SynchronizeAccountBalance را برای همگام نگه‌داشتن ستون‌های Balance معرفی می‌کنیم. کد شامل اسکript حذف scaffolding پس از پایان دورهٔ گذار هم هست:

sql
ALTER TABLE Account ADD Balance NUMBER(32,7);
COMMENT ON Account.Balance 'Moved from Customer table, finaldate = June 14 2007';

COMMENT ON Customer.Balance 'Moved to Account table, dropdate = June 14 2007';

CREATE OR REPLACE TRIGGER SynchronizeCustomerBalance
  BEFORE INSERT OR UPDATE
  ON Account
  REFERENCING OLD AS OLD NEW AS NEW
  FOR EACH ROW
  DECLARE
  BEGIN
    IF :NEW.Balance IS NOT NULL THEN
       UpdateCustomerBalance;
    END IF;
  END;

CREATE OR REPLACE TRIGGER SynchronizeAccountBalance
  BEFORE INSERT OR UPDATE OR DELETE
  ON Customer
  REFERENCING OLD AS OLD NEW AS NEW
  FOR EACH ROW
  DECLARE
  BEGIN
    IF DELETING THEN
       DeleteCustomerIfAccountNotFound;
    END IF;
    IF (UPDATING OR INSERTING) THEN
       IF :NEW.Balance IS NOT NULL THEN
         UpdateAccountBalanceForCustomer;
       END IF;
    END IF;
  END;

-- On June 14 2007
ALTER TABLE Customer DROP COLUMN Balance;
DROP TRIGGER SynchronizeCustomerBalance;
DROP TRIGGER SynchronizeAccountBalance;

مکانیک‌های مهاجرت داده

همهٔ داده از ستون اصلی به ستون جدید کپی شود — در این مورد از Customer.Balance به Account.Balance. این را می‌توان با چند روش انجام داد — مثلاً اسکript SQL یا ابزار ETL. (در این بازآرایی نباید گام transform باشد.) کد زیر DML انتقال مقادیر ستون Balance از Customer به Account را نشان می‌دهد:

sql
/*One-time migration of data from Customer.Balance to Account.Balance. When both the
 columns are
active, there is a need to have a trigger that keeps both the balance columns in sync
*/

UPDATE Account SET Balance =
  (SELECT Balance FROM Customer
    WHERE CustomerID = Account.CustomerID);

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

باید برنامه‌های دسترسی را کامل بررسی و در دورهٔ گذار به‌روز کنید. به‌روزرسانی‌های بالقوه:

  1. بازکار joinها برای استفاده از ستون منتقل‌شده. joinها — چه در SQL سخت‌کد شده چه در متادیتا — باید با ستون منتقل‌شده کار کنند. مثلاً وقتی Customer.Balance به Account.Balance منتقل می‌شود، پرس‌وجوها باید موجودی را از Account بگیرند نه Customer.

  2. جدول جدید را به joinها اضافه کنید. اگر Account از قبل در joinها نیست، اکنون باید اضافه شود — ممکن است عملکرد را کاهش دهد.

  3. جدول اصلی را از joinها حذف کنید. ممکن است joinهایی Customer را فقط برای Customer.Balance شامل کرده باشند. اکنون که ستون منتقل شده، Customer را می‌توان از join حذف کرد — که شاید عملکرد را بهبود دهد.

کد زیر نشان می‌دهد Customer.Balance در کد اصلی چگونه ارجاع داده می‌شود و به‌روزرسانی که با Account.Balance کار می‌کند:

java
//Before code
      public BigDecimal getCustomerBalance(Long
      customerId) throws SQLException {
        PreparedStatement stmt = null;
        BigDecimal customerBalance = null;
        stmt = DB.prepare("SELECT Balance FROM
        Customer " +
                "WHERE CustomerId = ?");
        stmt.setLong(1, customerId.longValue());
        ResultSet rs = stmt.executeQuery();
        if (rs.next()) {
          customerBalance = rs.getBigDecimal("Balance");
        }
        return customerBalance;
}

//After code
      public BigDecimal getCustomerBalance(Long
      customerId) throws SQLException {
        PreparedStatement stmt = null;
        BigDecimal customerBalance = null;
        stmt = DB.prepare(
           "SELECT SUM(Account.Balance) Balance " +
           "FROM Customer, Account " +
           "WHERE Customer.CustomerId=
           Account.CustomerId " +
           "AND CustomerId = ?");
        stmt.setLong(1, customerId.longValue());
        ResultSet rs = stmt.executeQuery();
        if (rs.next()) {
          customerBalance = rs.getBigDecimal("Balance");
        }
        return customerBalance;
}

Rename Column (تغییر نام ستون)

تغییر نام یک ستون موجود در جدول.

انگیزه

دلایل اصلی اعمال Rename Column افزایش خوانایی طرحوارهٔ پایگاه داده، انطباق با قراردادهای نام‌گذاری پذیرفته‌شده در سازمان، یا امکان port کردن پایگاه داده است. مثلاً هنگام port از یک محصول پایگاه داده به دیگری ممکن است ببینید نام ستون اصلی به‌خاطر کلمهٔ رزرو شده در پایگاه دادهٔ جدید قابل استفاده نیست.

مبادلات بالقوه

مبادلهٔ اصلی هزینهٔ بازآرایی برنامه‌های بیرونی که به ستون دسترسی دارند در برابر خوانایی و/یا یکنواختی بهتر نام جدید است.

مکانیک‌های به‌روزرسانی طرحواره

برای تغییر نام ستون باید کارهای زیر را انجام دهید:

  1. ستون جدید را معرفی کنید. در شکل ۶.۹ ابتدا FirstName را با دستور SQL ADD COLUMN به جدول هدف اضافه می‌کنیم.

شکل ۶.۹. تغییر نام ستون Customer.FName.

  1. trigger همگام‌سازی معرفی کنید. همان‌طور که در شکل ۶.۹ می‌بینید، trigger لازم است تا در دورهٔ گذار داده را از یک ستون به دیگری کپی کند. این trigger باید با هر تغییر سطر فراخوانی شود.

  2. ستون‌های دیگر را تغییر نام دهید. اگر FName در جداول دیگر به‌عنوان (بخشی از) کلید خارجی استفاده می‌شود، شاید بخواهید Rename Column را به‌صورت بازگشتی برای یکنواختی نام‌گذاری اعمال کنید. مثلاً اگر Customer.CustomerNumber به Customer.CustomerID تغییر نام یابد، شاید بخواهید همهٔ نمونه‌های CustomerNumber در جداول دیگر را هم تغییر نام دهید. بنابراین Account.CustomerNumber اکنون Account.CustomerID می‌شود.

کد زیر DDL تغییر نام Customer.FName به Customer.FirstName، ایجاد trigger SynchronizeFirstName برای همگام‌سازی در دورهٔ گذار، و حذف ستون و trigger اصلی پس از پایان دورهٔ گذار را نشان می‌دهد:

sql
ALTER TABLE Customer ADD FirstName VARCHAR(40);

COMMENT ON Customer.FirstName 'Renaming of FName column, finaldate = November 14 2007';

COMMENT ON Customer.FName 'Renamed to FirstName,
dropdate = November 14 2007';

UPDATE Customer SET FirstName = FName;

CREATE OR REPLACE TRIGGER SynchronizeFirstName
BEFORE INSERT OR UPDATE
ON Customer
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
  IF INSERTING THEN
    IF :NEW.FirstName IS NULL THEN
      :NEW.FirstName := :NEW.FName;
    END IF;
    IF :NEW.Fname IS NULL THEN
      :NEW.FName := :NEW.FirstName;
    END IF;
  END IF;

    IF UPDATING THEN
      IF NOT(:NEW.FirstName=:OLD.FirstName) THEN
         :NEW.FName:=:NEW.FirstName;
      END IF;
      IF NOT(:NEW.FName=:OLD.FName) THEN
         :NEW.FirstName:=:NEW.FName;
      END IF;
    END IF;
    END;
/

-- On Nov 30 2007
DROP TRIGGER SynchronizeFirstName;
ALTER TABLE Customer DROP COLUMN FName;

مکانیک‌های مهاجرت داده

باید همهٔ داده از ستون اصلی به ستون جدید کپی شود — در این مورد از FName به FirstName. جزئیات را در بازآرایی Move Data (انتقال داده) ببینید.

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

برنامه‌های بیرونی که به Customer.FName ارجاع می‌دهند باید ستون را با نام جدیدش ارجاع دهند. معمولاً کافی است SQL تعبیه‌شده و/یا متادیتای نگاشت را به‌روز کنید. فایل‌های نگاشت Hibernate زیر نشان می‌دهند هنگام تغییر نام ستون fName نگاشت‌ها چگونه تغییر می‌کنند:

xml
//Before mapping
<hibernate-mapping>
<class name="Customer" table="Customer">
  <id name="id" column="CUSTOMERID">
         <generator class="CustomerIdGenerator"/>
  </id>
  <property name="fName"/>
</class>
</hibernate-mapping>
//Transition mapping
<hibernate-mapping>
<class name="Customer" table="Customer">
  <id name="id" column="CUSTOMERID">
         <generator class="CustomerIdGenerator"/>
  </id>
  <property name="fName"/>
  <property name="firstName"/>
</class>
</hibernate-mapping>

//After mapping
<hibernate-mapping>

<class name="Customer" table="Customer">
  <id name="id" column="CUSTOMERID">
         <generator class="CustomerIdGenerator"/>
  </id>
  <property name="firstName"/>
</class>
</hibernate-mapping>

Rename Table (تغییر نام جدول)

تغییر نام یک جدول موجود.

انگیزه

دلیل اصلی اعمال Rename Table روشن‌تر کردن معنا و منظور جدول در طرحوارهٔ کلی پایگاه داده یا انطباق با قراردادهای نام‌گذاری پذیرفته‌شده است. ایده‌آل این است که این دو دلیل یکی باشند.

مبادلات بالقوه

مبادلهٔ اصلی هزینهٔ بازآرایی برنامه‌های بیرونی که به جدول دسترسی دارند در برابر خوانایی و/یا یکنواختی بهتر نام جدید است.

مکانیک‌های به‌روزرسانی طرحواره از طریق جدول جدید

برای Rename Table جدول جدیدی با دستور SQL CREATE TABLE می‌سازید — در این مورد Customer. اگر هر ستونی از Cust_TB_Prod در جداول دیگر به‌عنوان (بخشی از) کلید خارجی استفاده می‌شود، باید آن محدودیت‌ها و/یا indexهای پیاده‌کنندهٔ کلید خارجی را بازآرایی کنید تا به Customer اشاره کنند.

می‌خواهیم Cust_TB_Prod را به Customer تغییر نام دهیم، همان‌طور که در شکل ۶.۱۰ نشان داده شده. triggerهای SynchronizeCust_TB_Prod و SynchronizeCustomer دو جدول را با هم همگام نگه می‌دارند. هر trigger با هر تغییر سطر در Cust_TB_Prod یا Customer فراخوانی می‌شود. کد زیر DDL تغییر نام جدول و معرفی triggerها را نشان می‌دهد:

شکل ۶.۱۰. تغییر نام جدول Cust_TB_Prod به Customer.

sql
CREATE TABLE Customer
   (FirstName VARCHAR(40),
   LastName VARCHAR(40),
);

COMMENT ON Customer 'Renaming of Cust_TB_Prod,
finaldate = September 14 2006';

COMMENT ON Cust_TB_Prod 'Renamed to Customer,
dropdate = September 14 2006';

CREATE OR REPLACE TRIGGER SynchronizeCustomer
BEFORE INSERT OR UPDATE
ON Cust_TB_Prod
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
IF updating THEN
  findAndUpdateIfNotFoundCreateCustomer;
END IF;
IF inserting THEN
  createNewIntoCustomer;
END IF;
IF deleting THEN
  deleteFromCustomer;
END IF;
END;
/

CREATE OR REPLACE TRIGGER SynchronizeCust_TB_Prod
BEFORE INSERT OR UPDATE
ON Customer
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
IF updating THEN
        findAndUpdateIfNotFoundCreateCust_TB_Prod;
END IF;
IF inserting THEN
  createNewIntoCust_TB_Prod;
END IF;
IF deleting THEN
  deleteFromCust_TB_Prod;
END IF;
END;
/

مکانیک‌های به‌روزرسانی طرحواره از طریق نمای قابل‌به‌روزرسانی

رویکرد دوم تغییر نام جدول و سپس معرفی نمای قابل‌به‌روزرسانی با نام اصلی جدول است. توجه کنید برخی محصولات پایگاه داده گزینهٔ RENAME دستور ALTER TABLE را پشتیبانی می‌کنند. اگر محصول شما نه، باید جدول را با نام جدید دوباره بسازید و داده را بارگذاری کنید. حذف جدول قدیمی را برنامه‌ریزی کنید تا کسی اشتباهی از آن استفاده نکند. همان‌طور که در شکل ۶.۱۱ می‌بینید، نمای قابل‌به‌روزرسانی در دورهٔ گذار برای پشتیبانی از برنامه‌های دسترسی بیرونی که هنوز بازآرایی نشده‌اند لازم است. این استراتژی فقط وقتی عملی است که پایگاه دادهٔ شما نماهای قابل‌به‌روزرسانی را پشتیبانی کند.

شکل ۶.۱۱. تغییر نام جدول Cust_TB_Prod به Customer از طریق نما.

شکل ۶.۱۱ نشان می‌دهد چگونه با نما جدول را تغییر نام دهید. کافی است از بند RENAME TO دستور ALTER TABLE SQL برای تغییر نام جدول استفاده کنید و نما را بسازید:

sql
ALTER TABLE Cust_tb_Prod RENAME TO Customer;

CREATE VIEW Cust_tb_Prod AS
  SELECT * FROM Customer;

مثل رویکرد جدول جدید، اگر هر ستونی از Cust_TB_Prod در جداول دیگر به‌عنوان (بخشی از) کلید خارجی استفاده می‌شود، باید آن محدودیت‌ها و/یا indexها را دوباره بسازید تا به Customer اشاره کنند.

مکانیک‌های مهاجرت داده

با رویکرد نمای قابل‌به‌روزرسانی نیازی به مهاجرت داده نیست. اما با رویکرد جدول جدید، ابتدا باید داده را کپی کنید — همهٔ داده از جدول اصلی به جدول جدید، در این مورد از Cust_TB_Prod به Customer. دوم، trigger روی هر دو جدول لازم است تا در دورهٔ گذار داده را از یک جدول به دیگری کپی کند. triggerها باید با هر تغییر جدول فراخوانی شوند. triggerها را طوری پیاده کنید که چرخه ایجاد نشود — اگر Cust_TB_Prod تغییر کند، Customer هم باید به‌روز شود، اما آن به‌روزرسانی نباید همان به‌روزرسانی را روی Cust_TB_Prod دوباره trigger کند. کد زیر نشان می‌دهد چگونه داده از Cust_TB_Prod به Customer کپی شود:

sql
INSERT INTO Customer
  SELECT * FROM CUST_TB_PROD;

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

برنامه‌های دسترسی بیرونی باید با Customer به‌جای Cust_TB_Prod بازآرایی شوند. نگاشت Hibernate زیر تغییر لازم هنگام تغییر نام جدول Cust_TB_Prod را نشان می‌دهد:

xml
//Before mapping
<hibernate-mapping>
<class name="Customer" table="Cust_TB_Prod">
.....
</class>
</hibernate-mapping>

//After mapping
<hibernate-mapping>
<class name="Customer" table="Customer">
.....
</class>
</hibernate-mapping>

Rename View (تغییر نام نما)

تغییر نام یک نمای موجود.

انگیزه

دلیل اصلی اعمال Rename View افزایش خوانایی طرحوارهٔ پایگاه داده یا انطباق با قراردادهای نام‌گذاری پذیرفته‌شده است. ایده‌آل این است که این دو دلیل یکی باشند.

مبادلات بالقوه

مبادلهٔ اصلی هزینهٔ بازآرایی برنامه‌های بیرونی که به نما دسترسی دارند در برابر خوانایی و/یا یکنواختی بهتر نام جدید است.

مکانیک‌های به‌روزرسانی طرحواره

برای Rename View باید کارهای زیر را انجام دهید:

  1. نمای جدید را معرفی کنید. نمای جدیدی با دستور SQL CREATE VIEW بسازید. در شکل ۶.۱۲ این CustomerOrders است که تعریفش باید با CustOrds یکسان باشد.

شکل ۶.۱۲. تغییر نام نمای CustOrds به CustomerOrders.

  1. نمای اصلی را منسوخ کنید. پس از ایجاد CustomerOrders باید نشان دهید CustOrds دیگر نباید با قابلیت یا رفع باگ جدید به‌روز شود.

  2. نمای قدیمی را بازتعریف کنید. CustOrds را طوری بازتعریف کنید که بر CustomerOrders استوار باشد تا جریان کد تکراری نداشته باشید. مزیت این است که هر تغییر در CustomerOrders — مثلاً منبع دادهٔ جدید برای ستون — بدون کار اضافه به CustOrds منتقل می‌شود.

کد زیر DDL ایجاد CustomerOrders — که با کد ایجاد CustOrds یکسان است — را نشان می‌دهد:

sql
CREATE VIEW CustomerOrders AS
SELECT
  Customer.CustomerCode,
  Order.OrderID,
  Order.OrderDate,
  Order.ProductCode
FROM Customer,Order
WHERE
  Customer.CustomerCode = Order.CustomerCode
  AND Order.ShipDate = TOMORROW
;

COMMENT ON CustomerOrders 'Renamed from CustOrds,
CustOrds dropdate = September 15 2007';

کد زیر CustOrds را حذف و دوباره می‌سازد تا نتایجش را از CustomerOrders بگیرد:

sql
DROP VIEW CustOrds;

CREATE VIEW CustOrds AS
SELECT CustomerCode, OrderID, OrderDate, ProductCode
FROM CustomerOrders
;

مکانیک‌های مهاجرت داده

برای این بازآرایی پایگاه داده داده‌ای برای مهاجرت نیست.

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

باید همهٔ برنامه‌های بیرونی که اکنون به CustOrds دسترسی دارند بازآرایی شوند تا به CustomerOrders دسترسی پیدا کنند. در SQL سخت‌کد شده، این ساده به‌روزرسانی بندهای FROM/INTO است و در رویکرد مبتنی بر متادیتا، به‌روزرسانی نام در representation این نما.

کد زیر نشان می‌دهد ارجاع به CustOrds چگونه باید به CustomerOrders تغییر کند:

java
// Before code
stmt.prepare(
  "SELECT * " +
  "FROM CustOrds "+
  "WHERE CustomerId = ?");
  stmt.setLong(1,customer.getCustomerID);
  stmt.execute();
  ResultSet rs = stmt.executeQuery();

// After code
stmt.prepare(
  "SELECT * " +
  "FROM CustomerOrders " +
  "WHERE " +
  " CustomerId = ?");
  stmt.setLong(1,customer.getCustomerID);
  stmt.execute();
  ResultSet rs = stmt.executeQuery();

Replace Large Object (LOB) With Table (جایگزینی شیء بزرگ با جدول)

جایگزینی ستون شیء بزرگ (LOB) که دادهٔ ساخت‌یافته دارد با جدول جدید یا ستون‌های جدید در همان جدول. LOBها معمولاً به‌صورت binary large object (BLOB)، variable character (VARCHAR) یا در برخی موارد دادهٔ XML ذخیره می‌شوند.

انگیزه

دلیل اصلی جایگزینی LOB با جدول این است که بخش‌هایی از LOB را باید به‌عنوان عناصر دادهٔ مجزا در نظر بگیرید. این با ساختارهای XML که در یک ستون ذخیره شده‌اند — اغلب برای اجتناب از «shred» کردن ساختار به ستون‌های جدا — بسیار رایج است.

مبادلات بالقوه

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

مکانیک‌های به‌روزرسانی طرحواره

همان‌طور که در شکل ۶.۱۳ می‌بینید، اعمال Replace LOB With Table ساده است. باید کارهای زیر را انجام دهید:

شکل ۶.۱۳. جایگزینی LOB با جدول.

  1. طرحوارهٔ جدول را تعیین کنید. Customer.MailingAddress را تحلیل کنید تا دادهٔ داخلش را بفهمید و طرحوارهٔ جدول برای ذخیرهٔ آن بسازید. اگر ساختار داخل MailingAddress پیچیده است، یا ستون‌های LOB کوچک‌تر در جدول جدید لازم است یا به‌صورت بازگشتی Replace LOB With Table را برای آن ساختارهای کوچک‌تر اعمال کنید.

  2. جدول را اضافه کنید. در شکل ۶.۱۳ این CustomerAddress است. ستون‌های این جدول کلید اصلی Customer، ستون CustomerPOID و ستون‌های جدید حاوی داده از MailingAddress هستند.

  3. ستون اصلی را منسوخ کنید. MailingAddress باید برای حذف در پایان دورهٔ منسوخ‌شدن علامت بخورد.

  4. index جدید اضافه کنید. به‌دلایل عملکرد، شاید index جدید برای CustomerAddress با دستور CREATE INDEX لازم باشد.

  5. triggerهای همگام‌سازی معرفی کنید. Customer trigger لازم دارد تا مقادیر CustomerAddress را مناسب پر کند — trigger باید ساختار MailingAddress را shred و مناسب ذخیره کند. به‌همین شکل trigger روی CustomerAddress برای به‌روزرسانی Customer در دورهٔ گذار لازم است.

کد ایجاد جدول CustomerAddress، افزودن index، تعریف triggerهای همگام‌سازی و در نهایت حذف ستون و triggerها:

sql
CREATE TABLE CustomerAddress (
   CustomerPOID NUMBER NOT NULL,
   Street VARCHAR(40),
   City VARCHAR(40),
   State VARCHAR(40),
   ZipCode VARCHAR(10)
);

CREATE INDEX IndexCustomerAddress ON
  CustomerAddress(CustomerPOID);

-- Triggers to keep the tables synchronized
CREATE OR REPLACE TRIGGER SynchronizeWithCustomerAddress
BEFORE INSERT OR UPDATE OR DELETE

ON Customer
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
  IF updating THEN
     FindOrCreateCustomerAddress;
  END IF;
  IF inserting THEN
     CreateCustomerAddress;
  END IF;
  IF deleting THEN
     DeleteCustomerAddress;
  END IF;
END;
/

CREATE OR REPLACE TRIGGER SynchronizeWithCustomer
BEFORE INSERT OR UPDATE OR DELETE
ON Customer
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
  IF updating OR inserting THEN
     FindAndUpdateCustomer;
  END IF;
  IF deleting THEN
     UpdateCustomerNullAddress;
  END IF;
END;
/

-- On Dec 14 2007
DROP TRIGGER SynchronizeWithCustomerAddress;
DROP TRIGGER SynchronizeWithCustomer;

ALTER TABLE Customer DROP COLUMN MailingAddress;

مکانیک‌های مهاجرت داده

CustomerAddress باید با shred و کپی دادهٔ Customer.MailingAddress پر شود. مقدار Customer.CustomerPOID هم برای حفظ رابطه کپی می‌شود. اگر MailingAddress مقدار NULL یا خالی دارد، لازم نیست سطری در CustomerAddress ساخته شود. این را می‌توان با یک یا چند اسکript SQL انجام داد:

sql
INSERT INTO CustomerAddress
  SELECT
    CustomerPOID,
    ExtractStreet(MailingAddress),
    ExtractCity(MailingAddress),
    ExtractState(MailingAddress),
    ExtractZipCode(MailingAddress)
  FROM Customer
    WHERE MailingAddress IS NOT NULL;

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

هر برنامهٔ بیرونی که به Customer.MailingAddress ارجاع می‌دهد را شناسایی کنید تا در صورت لزوم با CustomerAddress کار کند. باید کارهای زیر را انجام دهید:

  1. کد ترجمه را حذف کنید. برنامه‌های بیرونی ممکن است کدی داشته باشند که دادهٔ داخل MailingAddress را shred می‌کند یا عناصر مبدأ را به فرمت ذخیره در MailingAddress می‌سازد. با ساختار دادهٔ جدید این کد دیگر لازم نیست.

  2. کد ترجمه اضافه کنید. برعکس، برخی برنامه‌ها ممکن است ساختار دقیق داخل MailingAddress را بخواهند. اگر چند برنامه نیاز دارند، stored procedure یا کتابخانه‌ای در پایگاه داده برای ترجمه و استفادهٔ مجدد در نظر بگیرید.

  3. کد دسترسی به جدول جدید بنویسید. پس از افزودن CustomerAddress باید کد برنامه‌ای بنویسید که از این جدول به‌جای MailingAddress استفاده کند.

کد زیر نشان می‌دهد چگونه کد بازیابی ویژگی‌های داده از Customer.MailingAddress با SELECT روی جدول Customer-Address جایگزین می‌شود:

java
// Before code
public Customer findByCustomerID(Long customerPOID) {
  Customer customer = new Customer();
  stmt = DB.prepare("SELECT CustomerPOID, " +
    "MailingAddress, Name, PhoneNumber " +
    "FROM Customer " +
    "WHERE CustomerPOID = ?");
  stmt.setLong(1, customerPOID);
  stmt.execute();
  ResultSet rs = stmt.executeQuery();
  if (rs.next()) {
    customer.setCustomerId(rs.getLong("CustomerPOID"));
    customer.setName(rs.getString("Name"));
    customer.setPhoneNumber(rs.getString
    ("PhoneNumber"));
    String mailingAddress = rs.getString
    ("MailingAddress");
    customer.setStreet(extractStreet(mailingAddress));
    customer.setCity(extractCity(mailingAddress));
    customer.setState(extractState(mailingAddress));
    customer.setZipCode(extractZipCode(mailingAddress));
  }
return customer;
}
// After code
public Customer findByCustomerID(Long customerPOID) {
  Customer customer = new Customer();
  stmt = DB.prepare("SELECT CustomerPOID, "+
    "Name, PhoneNumber, "+
    "Street, City, State, ZipCode " +
  "FROM Customer, CustomerAddress " +
  "WHERE Customer.CustomerPOID = ? " +
  "AND Customer.CustomerPOID =
  CustomerAddress.CustomerPOID");
  stmt.setLong(1, customerPOID);
  stmt.execute();
  ResultSet rs = stmt.executeQuery();
  if (rs.next()) {
    customer.setCustomerId(rs.getLong("CustomerPOID"));

    customer.setName(rs.getString("Name"));
    customer.setPhoneNumber(rs.getString
    ("PhoneNumber"));
    customer.setStreet(rs.getString("Street"));
    customer.setCity(rs.getString("City"));
    customer.setState(rs.getSring("State"));
    customer.setZipCode(rs.getString("ZipCode"));
  }
return customer;
}

Replace Column (جایگزینی ستون)

جایگزینی یک ستون غیرکلیدی موجود با ستون جدید.

برای جایگزینی ستونی که بخشی از کلید است — چه کلید اصلی چه کلید جایگزین — بازآرایی‌های Introduce Surrogate Key (معرفی کلید جایگزین) و Replace Surrogate Key With Natural Key (جایگزینی کلید جایگزین با کلید طبیعی) را ببینید.

انگیزه

دو دلیل برای اعمال Replace Column وجود دارد. اول و رایج‌ترین این است که استفاده از ستون با گذشت زمان تغییر کرده و باید نوعش عوض شود. مثلاً قبلاً شناسهٔ عددی مشتری داشتید، اما ذی‌نفعان کسب‌وکار آن را الفبایی-عددی کرده‌اند. دوم، این ممکن است گام میانی برای بازآرایی‌های دیگر باشد. دلیل رایج دیگر جایگزینی ستون موجود این است که اغلب گام مهمی در ادغام دو منبع دادهٔ مشابه یا اعمال Consolidate Key Strategy (یکپارچه‌سازی استراتژی کلید) است چون باید یکنواختی نوع و قالب با ستون دیگر تضمین شود.

مبادلات بالقوه

ریسک قابل‌توجه هنگام جایگزینی ستون، از دست رفتن اطلاعات هنگام انتقال داده به ستون جایگزین است. این به‌ویژه وقتی نوع دو ستون تفاوت زیادی دارد درست است — تبدیل CHAR به VARCHAR ساده است، NUMERIC به CHAR هم، اما CHAR به NUMERIC وقتی ستون اصلی کاراکتر غیرعددی دارد مشکل‌ساز است.

مکانیک‌های به‌روزرسانی طرحواره

برای Replace Column باید کارهای زیر را انجام دهید:

  1. ستون جدید را معرفی کنید. ستون را با دستور SQL ADD COLUMN به جدول هدف اضافه کنید. در شکل ۶.۱۴ این CustomerID است.

شکل ۶.۱۴. جایگزینی ستون Customer.CustomerNumber.

  1. ستون اصلی را منسوخ کنید. CustomerNumber باید برای حذف در پایان دورهٔ گذار انتخاب‌شده علامت بخورد.

  2. trigger همگام‌سازی معرفی کنید. همان‌طور که در شکل ۶.۱۴ می‌بینید، trigger لازم است تا در دورهٔ گذار داده را از یک ستون به دیگری کپی کند. این trigger باید با هر تغییر سطر فراخوانی شود.

  3. جداول دیگر را به‌روز کنید. اگر CustomerNumber در جداول دیگر بخشی از کلید خارجی است، آن ستون‌ها را به‌همین شکل جایگزین کنید و تعاریف index متناظر را به‌روز کنید.

کد SQL زیر DDL جایگزینی ستون، ایجاد trigger همگام‌سازی، و در نهایت حذف ستون و trigger پس از دورهٔ گذار را نشان می‌دهد:

sql
ALTER TABLE Customer ADD CustomerID CHAR(12);

COMMENT ON Customer.CustomerID 'Replaces CustomerNumber column, finaldate = 2007-06-14';

COMMENT ON Customer.CustomerNumber 'Replaced with CustomerID, dropdate = 2007-06-14';

CREATE OR REPLACE TRIGGER SynchronizeCustomerIDNumber
BEFORE INSERT OR UPDATE
ON Customer
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
  BEGIN
  IF :NEW.CustomerID IS NULL THEN
    :NEW.CustomerID:=
      formatCustomerNumber(:New.CustomerNumber);
  END IF;
  IF :NEW.CustomerNumber IS NULL THEN

     :New.CustomerNumber := :New.CustomerID;
  END IF;
END;
/

-- On June 14 2007
DROP TRIGGER SynchronizeCustomerIDNumber;
ALTER TABLE Customer DROP COLUMN CustomerNumber;

مکانیک‌های مهاجرت داده

داده باید ابتدا از CustomerNumber به CustomerID کپی شود و سپس در دورهٔ گذار همگام بماند (مثلاً از طریق stored procedure). همان‌طور که گفته شد، وقتی قالب‌های داده تفاوت زیادی دارند این مشکل‌ساز است. قبل از Replace Column ممکن است لازم باشد یک یا چند بازآرایی کیفیت داده برای پاک‌سازی دادهٔ مبدأ اعمال کنید. کد کپی مقادیر به ستون جدید:

sql
UPDATE Customer SET CustomerID = CustomerNumber;

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

مسئلهٔ اصلی این است که برنامه‌های بیرونی باید با نوع و قالب دادهٔ جدید CustomerID بازآرایی شوند. این ممکن است به معنای نوشتن کد تبدیل بین قالب قدیم و جدید باشد. استراتژی بلندمدت — اگرچه احتمالاً گران‌تر — بازکار کامل همهٔ کد برنامهٔ بیرونی برای استفاده از قالب جدید است. قطعهٔ کد زیر نشان می‌دهد نام ستون و نوع داده در کد برنامه چگونه باید تغییر کند:

java
// Before code
public Customer findByCustomerID(Long customerID) {
  Customer customer = new Customer();
  stmt = DB.prepare("SELECT CustomerPOID, " +
    "CustomerNumber, FirstName, LastName " +
    "FROM Customer " +
    "WHERE CustomerPOID = ?");
  stmt.setLong(1, customerID);
  stmt.execute();
  ResultSet rs = stmt.executeQuery();
  if (rs.next()) {
    customer.setCustomerPOID(rs.getLong
("CustomerPOID"));
    customer.setCustomerNumber(rs.getInt
("CustomerNumber"));
    customer.setFirstName(rs.getString("FirstName"));
    customer.setLastName(rs.getString("LastName"));
  }
return customer;
}

// After code
public Customer findByCustomerID(Long customerID) {
  Customer customer = new Customer();
  stmt = DB.prepare("SELECT CustomerPOID, " +
    "CustomerID, FirstName, LastName " +
    "FROM Customer " +
    "WHERE CustomerPOID = ?");

  stmt.setLong(1, customerID);
  stmt.execute();
  ResultSet rs = stmt.executeQuery();
  if (rs.next()) {
    customer.setCustomerPOID(rs.getLong
("CustomerPOID"));
    customer.setCustomerID(rs.getString("CustomerID"));
    customer.setFirstName(rs.getString("FirstName"));
    customer.setLastName(rs.getString("LastName"));
  }
return customer;
}

Replace One-To-Many With Associative Table (جایگزینی یک-به-چند با جدول واسط)

جایگزینی ارتباط یک-به-چند بین دو جدول با جدول واسط (associative table).

انگیزه

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

مبادلات بالقوه

وقتی از جدول واسط برای پیاده‌سازی یک-به-چند استفاده می‌کنید، طرحواره را بیش‌ازحد طراحی می‌کنید. اگر ارتباط احتمالاً به many-to-many تکامل نیابد، این رویکرد توصیه نمی‌شود. با افزودن جداول واسط، تعداد joinهای لازم برای رسیدن به دادهٔ مرتبط بیشتر می‌شود — عملکرد را کاهش می‌دهد و فهم طرحواره را سخت‌تر می‌کند.

مکانیک‌های به‌روزرسانی طرحواره

برای Replace One-To-Many With Associative Table باید کارهای زیر را انجام دهید:

  1. جدول واسط را اضافه کنید. در شکل ۶.۱۵ این Holds است. ستون‌های این جدول ترکیب کلیدهای اصلی Customer و Policy است. توجه کنید برخی جداول لزوماً کلید اصلی ندارند — اگرچه نادر است — در آن صورت شاید Introduce Surrogate Key را اعمال کنید.

شکل ۶.۱۵. جایگزینی یک-به-چند با جدول واسط.

  1. ستون اصلی را منسوخ کنید. چون دیگر رابطه را مستقیماً از Policy به Customer نگه نمی‌داریم، Policy.CustomerPOID باید برای حذف در پایان دورهٔ گذار علامت بخورد — اکنون برای نگه‌داشتن یک-به-چند با Customer استفاده می‌شود اما دیگر لازم نخواهد بود.

  2. index جدید اضافه کنید. index جدید برای Holds باید از طریق بازآرایی Introduce Index (معرفی index) معرفی شود.

  3. triggerهای همگام‌سازی معرفی کنید. Policy trigger لازم دارد که در دورهٔ گذار مقادیر کلید را در جدول Holds پر کند — اگر مقادیر مناسب از قبل وجود نداشته باشد. به‌همین شکل trigger روی Holds لازم است که Policy.CustomerPOID را مناسب پر کند.

کد افزودن جدول Holds، index روی Holds، triggerهای همگام‌سازی و در نهایت حذف طرحواره و triggerهای قدیمی:

sql
CREATE TABLE Holds (
   CustomerPOID BIGINT,
   PolicyID INT,
);
CREATE INDEX HoldsIndex ON Holds
     (CustomerPOID, PolicyID);

CREATE OR REPLACE TRIGGER InsertHoldsRow
BEFORE INSERT OR UPDATE OR DELETE
ON Policy
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
  IF updating THEN
    UpdateInsertHolds;
  END IF;
  IF inserting THEN
    CreateHolds;
  END IF;
  IF deleting THEN
    RemoveHolds;
  END IF;
END;

/

CREATE OR REPLACE TRIGGER UpdatePolicyCustomerPOID
BEFORE INSERT OR UPDATE OR DELETE
ON Holds
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
BEGIN
IF updating THEN
  UpdateInsertPolicy;
END IF;
IF inserting THEN
  CreatePolicy;
END IF;
IF deleting THEN
  RemovePolicy;
END IF;
END;
/

-- On Mar 15 2007
DROP TRIGGER InsertHoldsRow;
DROP TRIGGER UpdatePolicyCustomerPOID;
ALTER TABLE customer
DROP COLUMN balance;

تعیین قرارداد نام‌گذاری

دو قرارداد نام‌گذاری رایج برای جداول واسط وجود دارد: یا همان نام ارتباط اصلی را به جدول بدهید — همان‌طور که اینجا کردیم — یا نام دو جدول را به هم بچسبانید که در این مورد CustomerPolicy می‌شد.

مکانیک‌های مهاجرت داده

جدول واسط باید با کپی مقادیر Policy.CustomerPOID و Policy.PolicyID به Holds.CustomerPOID و Holds.PolicyID پر شود. این را می‌توان با اسکript SQL ساده انجام داد:

sql
INSERT INTO Holds (CustomerPOID,PolicyID)
  SELECT CustomerPOID,PolicyID FROM Policy

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

برای به‌روزرسانی برنامه‌های بیرونی باید کارهای زیر را انجام دهید:

  1. به‌روزرسانی‌های کلید خارجی را حذف کنید. هر کدی که Policy.CustomerPOID را مقداردهی می‌کند باید بازآرایی شود تا برای حفظ ارتباط به Holds بنویسد.

  2. joinها را بازکار کنید. بسیاری از برنامه‌های دسترسی بیرونی joinهایی شامل Customer و Policy تعریف می‌کنند — با SQL سخت‌کد شده یا متادیتا. این joinها باید با Holds بازآرایی شوند.

  3. بازیابی‌ها را بازکار کنید. برخی برنامه‌ها پایگاه داده را سطر به سطر پیمایش می‌کنند و بر اساس مقادیر کلید از Policy به Customer می‌روند. این بازیابی‌ها به‌همین شکل باید به‌روز شوند.

کد زیر نشان می‌دهد چگونه کد برنامه را تغییر دهید تا بازیابی داده اکنون با join از جدول واسط انجام شود:

java
//Before code
stmt.prepare(
  "SELECT Customer.CustomerPOID, Customer.Name, " +
  "Policy.PolicyID,Policy.Amount " +
  "FROM Customer, Policy" +
  "WHERE Customer.CustomerPOID = Policy.CustomerPOID " +
  "AND Customer.CustomerPOID = ? ");
stmt.setLong(1,customerPOID);
ResultSet rs = stmt.executeQuery();

//After code
stmt.prepare(
  "SELECT Customer.CustomerPOID, Customer.Name, " +
  "Policy.PolicyID,Policy.Amount " +
  "FROM Customer, Holds, Policy" +
  "WHERE Customer.CustomerPOID = Holds.CustomerPOID " +
  "AND Holds.PolicyId = Policy.PolicyId " +
  "AND Customer.CustomerPOID = ? ");
stmt.setLong(1,customerPOID);
ResultSet rs = stmt.executeQuery();

Replace Surrogate Key With Natural Key (جایگزینی کلید جایگزین با کلید طبیعی)

جایگزینی کلید جایگزین با کلید طبیعی موجود. این بازآرایی مخالف Introduce Surrogate Key (معرفی کلید جایگزین) است.

انگیزه

چند دلیل برای اعمال Replace Surrogate Key With Natural Key وجود دارد:

  • کاهش overhead. وقتی کلید جایگزین را با کلید طبیعی موجود جایگزین می‌کنید، overhead نگه‌داشتن ستون(های) اضافی کلید جایگزین در ساختار جدول را کم می‌کنید.

  • یکپارچه‌سازی استراتژی کلید. برای پشتیبانی از Consolidate Key Strategy (یکپارچه‌سازی استراتژی کلید) ممکن است ابتدا تصمیم بگیرید کلید اصلی جایگزین موجود را با کلید طبیعی «رسمی» جایگزین کنید.

  • حذف کلیدهای غیرضروری. ممکن است ببینید کلید جایگزین وقتی واقعاً لازم نبود معرفی شده. همیشه بهتر است indexهای استفاده‌نشده را حذف کنید تا عملکرد بهبود یابد.

مبادلات بالقوه

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

مکانیک‌های به‌روزرسانی طرحواره

اعمال Replace Surrogate Key With Natural Key می‌تواند به‌خاطر coupling که کلید جایگزین احتمالاً دارد پیچیده باشد. چون کلید اصلی جدول است، احتمالاً (بخشی از) کلیدهای خارجی در جداول دیگر هم هست. باید کارهای زیر را انجام دهید:

  1. ستون(های) تشکیل‌دهندهٔ کلید اصلی جدید را شناسایی کنید. در شکل ۶.۱۶ این StateCode است (می‌تواند چند ستون باشد). StateCode باید در هر سطر مقدار یکتا داشته باشد تا واجد شرایط کلید اصلی باشد.

شکل ۶.۱۶. جایگزینی کلید جایگزین با کلید طبیعی.

  1. index جدید اضافه کنید. اگر از قبل وجود ندارد، index جدید بر اساس StateCode برای State معرفی شود.

  2. ستون اصلی را منسوخ کنید. StatePOID باید برای حذف در پایان دورهٔ گذار علامت بخورد.

  3. جداول وابسته را به‌روز کنید. اگر StatePOID در جداول دیگر بخشی از کلید خارجی است، آن جداول را برای استفاده از کلید جدید به‌روز کنید. باید ستون(های) متناظر StatePOID را با Drop Column (حذف ستون) حذف کنید. همچنین ستون(های) جدید متناظر StateCode را اضافه کنید اگر از قبل وجود ندارند. تعریف(های) index متناظر باید این تغییر را منعکس کند. وقتی StatePOID در جداول زیادی استفاده می‌شود، شاید بخواهید جداول را یکی‌یکی به‌روز کنید تا تلاش ساده‌تر شود.

  4. triggerهای RI را به‌روز و در صورت لزوم اضافه کنید. triggerهای موجود برای حفظ RI بین جداول باید با مقادیر StateCode متناظر در جداول دیگر کار کنند.

شکل ۶.۱۶ نشان می‌دهد چگونه State.StatePOID، کلید جایگزین، با State.StateCode موجود به‌عنوان کلید جایگزین شود. برای پشتیبانی از این کلید جدید، trigger PopulateStateCode را اضافه می‌کنیم که هر درج در Address رخ دهد فراخوانی می‌شود و مقدار State.StateCode را به‌دست می‌آورد:

sql
CREATE OR REPLACE TRIGGER PopulateStateCode

    BEFORE INSERT
    ON Address
    REFERENCING OLD AS OLD NEW AS NEW
    FOR EACH ROW
    DECLARE
    BEGIN
      IF :NEW.StateCode IS NULL THEN
         :NEW.StateCode := getStatePOIDFromState(StatePOID);
      END IF;
    END;
/

ALTER TABLE Address ADD (CONSTRAINT
AddressToStateForeignKey FOREIGN KEY (StateCode)
REFERENCES State;

-- June 14 2007
ALTER TABLE Address DROP CONSTRAINT
AddressToStateForeignKey;
ALTER TABLE State DROP CONSTRAINT StatePrimaryKey;
ALTER TABLE State MODIFY StateCode NOT NULL;
ALTER TABLE State ADD CONSTRAINT StatePrimaryKey
      PRIMARY KEY (StateCode);
DROP TRIGGER PopulateStateCode;

مکانیک‌های مهاجرت داده

برای این بازآرایی پایگاه داده داده‌ای برای مهاجرت نیست.

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

برای به‌روزرسانی برنامه‌های دسترسی بیرونی باید کارهای زیر را انجام دهید:

  1. کد کلید جایگزین را حذف کنید. کد اختصاص مقدار به ستون کلید جایگزین — چه در برنامه‌های بیرونی چه در پایگاه داده — دیگر نباید فراخوانی شود. شاید اصلاً دیگر لازم نباشد.

  2. join بر اساس کلید جدید. بسیاری از برنامه‌های دسترسی بیرونی joinهایی شامل State تعریف می‌کنند — با SQL سخت‌کد شده یا متادیتا. این joinها باید با StateCode بازآرایی شوند، نه StatePOID.

  3. بازیابی بر اساس کلید جدید. برخی برنامه‌ها پایگاه داده را سطر به سطر پیمایش می‌کنند و بر اساس مقادیر کلید داده بازیابی می‌کنند. این بازیابی‌ها به‌همین شکل باید به‌روز شوند.

نگاشت‌های Hibernate زیر نشان می‌دهند جداول ارجاع‌شده باید به کلیدهای جدید اشاره کنند و ستون‌های POID دیگر تولید نمی‌شوند:

xml
//Before mapping
<hibernate-mapping>
<class name="State" table="STATE">
  <id name="id" column="STATEPOID">
    <generator class="IdGenerator"/>
  </id>
  <property name="stateCode" />

  <property name="name" />
</class>
</hibernate-mapping>

<hibernate-mapping>
<class name="Address" table="ADDRESS">
  <id name="id" column="ADDRESSID" >
    <generator class="IdGenerator"/>
  </id>
  <property name="streetLine" />
  <property name="city" />
  <property name="postalCode" />
  <many-to-one name="state" class="State"
    column="STATEPOID" not-null="true"/>
  <many-to-one name="country" class="Country"
    column="COUNTRYID" not-null="true"/>
</class>
</hibernate-mapping>

//After mapping
<hibernate-mapping>
<class name="State" table="STATE">
  <property name="stateCode" />
  <property name="name" />
</class>
</hibernate-mapping>

<hibernate-mapping>
<class name="Address" table="ADDRESS">
  <id name="id" column="ADDRESSID" >
    <generator class="IdGenerator"/>
  </id>
  <property name="streetLine" />
  <property name="city" />
  <property name="postalCode" />
  <many-to-one name="state" class="State"
    column="STATECODE" not-null="true"/>
  <many-to-one name="country" class="Country"
    column="COUNTRYID" not-null="true"/>
</class>
</hibernate-mapping>

Split Column (تقسیم ستون)

تقسیم یک ستون به یک یا چند ستون در یک جدول.

یادداشت

اگر یکی یا چند ستون جدید باید در جدول دیگری ظاهر شوند، ابتدا Split Column را اعمال کنید و سپس Move Column (انتقال ستون).

انگیزه

دو دلیل برای اعمال Split Column وجود دارد. اول، نیاز به دادهٔ ریزدانه. مثلاً جدول Customer ستون Name دارد که نام کامل فرد را نگه می‌دارد، اما می‌خواهید آن را به FirstName، MiddleName و LastName به‌عنوان ستون‌های مستقل تقسیم کنید.

دوم، ستون چند کاربرد دارد. ستون اصلی برای ردیابی وضعیت Account معرفی شد و اکنون از آن برای نوع Account هم استفاده می‌شود. مثلاً Account.Status وضعیت حساب را نگه می‌دارد (Open، Closed، OverDrawn و غیره). ناخواسته شخص دیگری آن را برای نوع حساب (Checking، Savings و غیره) هم به‌کار برده. باید این کاربردها را در فیلدهای جدا split کنیم تا باگ از استفادهٔ دوگانه ایجاد نشود.

مبادلات بالقوه

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

مکانیک‌های به‌روزرسانی طرحواره

برای Split Column ابتدا ستون‌های جدید را معرفی کنید. ستون را با دستور SQL ADD COLUMN به جدول اضافه کنید. در شکل ۶.۱۷ این FirstName، MiddleName و LastName است. این گام اختیاری است چون شاید بتوانید از یکی از ستون‌های موجود برای split داده استفاده کنید. سپس trigger همگام‌سازی لازم است تا ستون‌ها با یکدیگر همگام بمانند. trigger باید با هر تغییر در ستون‌ها فراخوانی شود.

شکل ۶.۱۷. تقسیم ستون Customer.Name.

شکل ۶.۱۷ مثالی است که جدول Customer در ابتدا نام فرد را در ستون Name ذخیره می‌کند. فهمیدیم برنامه‌های کمی به نام کامل علاقه‌مندند، بلکه به اجزای آن — به‌ویژه نام خانوادگی — نیاز دارند. همچنین بسیاری از برنامه‌ها کد تکراری برای parse کردن Name دارند که منبع باگ بالقوه است. بنابراین Name را به FirstName، MiddleName و LastName split کردیم — منعکس‌کنندهٔ استفادهٔ واقعی. trigger SynchronizeCustomerName را برای همگام نگه‌داشتن مقادیر معرفی کردیم. کد زیر تغییرات طرحواره را پیاده می‌کند:

sql
ALTER TABLE Customer ADD FirstName VARCHAR(40);

COMMENT ON Customer.FirstName 'Added as the result of splitting Customer.Name finaldate =
December 14 2007';

ALTER TABLE Customer ADD MiddleName VARCHAR(40);

COMMENT ON Customer.MiddleName 'Added as the result of splitting Customer.Name finaldate =
December 14 2007';

ALTER TABLE Customer ADD LastName VARCHAR(40);

COMMENT ON Customer.LastName 'Added as the result of splitting Customer.Name finaldate =
 December
14 2007';

COMMENT ON Customer.Name 'Replaced with FirstName,
LastName and MiddleName, will be dropped December 14 2007';

-- Trigger to keep all the split columns in sync
CREATE OR REPLACE TRIGGER SynchronizeCustomerName
  BEFORE INSERT OR UPDATE
  ON Customer
  REFERENCING OLD AS OLD NEW AS NEW

  FOR EACH ROW
  DECLARE
  BEGIN
     IF :NEW.FirstName IS NULL THEN
       :NEW.FirstName := getFirstName(Name);
    END IF;
     IF :NEW.MiddleName IS NULL THEN
       :NEW.MiddleName := getMiddleName(Name);
    END IF;
     IF :NEW.LastName IS NULL THEN
       :NEW.LastName := getLastName(Name);
    END IF;
END;
/

-- On December 14 2007
ALTER TABLE Customer DROP COLUMN Name;
DROP TRIGGER SynchronizeCustomerName;

مکانیک‌های مهاجرت داده

باید همهٔ داده از ستون(های) اصلی به ستون‌های split‌شده کپی شود — در این مورد از Customer.Name به FirstName، MiddleName و LastName. کد زیر DML split اولیهٔ داده از Name به سه ستون را نشان می‌دهد (کد منبع سه stored function فراخوانی‌شده برای اختصار نشان داده نشده):

sql
/*One-time migration of data from Customer.Name to Customer.FirstName, Customer.MiddleName
, and
Customer.LastName. When both set of columns are active, there is a need to have a trigger that
keeps both set of columns in sync
*/

UPDATE Customer SET FirstName = getFirstName(Name);
UPDATE Customer SET MiddleName = getMiddleName(Name);
UPDATE Customer SET LastName = getLastName(Name);

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

باید برنامه‌های دسترسی را کامل بررسی و در دورهٔ گذار به‌روز کنید. علاوه بر نیاز واضح کار با FirstName، MiddleName و LastName به‌جای ستون(های) قبلی، به‌روزرسانی‌های بالقوه:

  1. کد split را حذف کنید. ممکن است کدی باشد که ستون(های) موجود را به ویژگی داده‌ای مشابه ستون‌های split‌شده تقسیم می‌کند. این کد باید بازآرایی و احتمالاً کاملاً حذف شود.

  2. کد اعتبارسنجی داده را برای دادهٔ split‌شده به‌روز کنید. برخی کدهای اعتبارسنجی فقط به‌خاطر split‌نشده بودن ستون‌ها وجود دارند. مثلاً اگر مقدار در Customer.Name ذخیره شده، کدی ممکن است بررسی کند که FirstName و LastName در مقدار هست. پس از split، شاید دیگر به این کد نیاز نباشد.

  3. رابط کاربری را بازآرایی کنید. پس از split ستون اصلی، لایهٔ presentation — اگر قبلاً نمی‌کرد — باید از دادهٔ ریزدانه استفاده کند.

کد زیر نشان می‌دهد برنامه چگونه از دادهٔ ریزدانه‌تر در دسترس استفاده می‌کند:

java
//Before code

public Customer findByCustomerID(Long customerID) {
Customer customer = new Customer();
stmt = DB.prepare("SELECT CustomerID, "+
"Name, PhoneNumber " +
"FROM Customer " +
"WHERE CustomerID = ?");
stmt.setLong(1, customerID);
stmt.execute();
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
  customer.setCustomerId(rs.getLong("CustomerID"));
  String name = rs.getString("Name");
  customer.setFirstName(getFirstName(name));
  customer.setMiddleName(getMiddleName(name));
  customer.setLastName(getMiddleName(name));
  customer.setPhoneNumber(rs.getString("PhoneNumber"));
}
return customer;
}

//After code
public Customer findByCustomerID(Long customerID) {
Customer customer = new Customer();
stmt = DB.prepare("SELECT CustomerID, "+
"FirstName, MiddleName, LastName, PhoneNumber " +
"FROM Customer " +
"WHERE CustomerID = ?");
stmt.setLong(1, customerID);
stmt.execute();
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
  customer.setCustomerId(rs.getLong("CustomerID"));
  customer.setFirstName(rs.getString("FirstName"));
  customer.setMiddleName(rs.getString("MiddleName"));
  customer.setLastName(rs.getString("LastName"));
  customer.setPhoneNumber(rs.getString("PhoneNumber"));
}
return customer;
}

Split Table (تقسیم جدول)

تقسیم عمودی (vertical split — مثلاً بر اساس ستون‌ها) یک جدول موجود به یک یا چند جدول.

یادداشت

اگر مقصد ستون‌های split جدول موجودی باشد، در واقع Move Column(s) (انتقال ستون) اعمال می‌کنید. برای split افقی جدول (مثلاً بر اساس سطرها) Move Data (انتقال داده) را اعمال کنید.

انگیزه

چند دلیل برای اعمال Split Table وجود دارد:

  • بهبود عملکرد. بسیار رایج است بیشتر برنامه‌ها برای هر موجودیت مجموعهٔ هسته‌ای از داده را بخواهند و زیرمجموعهٔ خاصی از ویژگی‌های غیرهسته. مثلاً ستون‌های هستهٔ Employee نام، آدرس و تلفن را ذخیره می‌کنند؛ ستون‌های غیرهسته شامل Picture و اطلاعات حقوق هستند. چون Employee.Picture بزرگ است و فقط چند برنامه به آن نیاز دارند، split آن به جدول جدا را در نظر بگیرید — زمان بازیابی برای برنامه‌هایی که همهٔ ستون‌ها را SELECT می‌کنند اما Picture لازم ندارند بهتر می‌شود.

  • محدود کردن دسترسی به داده. شاید بخواهید دسترسی به برخی ستون‌ها — مثلاً اطلاعات حقوق در Employee — را با split به جدول جدا و اعمال قواعد SAC (Security Access Control) خاص محدود کنید.

  • کاهش گروه‌های دادهٔ تکراری (اعمال 1NF). جدول اصلی شاید وقتی نیازمندی‌ها نهایی نشده یا توسط کسانی طراحی شده که چرایی نرمال‌سازی ساختار داده را نمی‌دانستند (Date 2001) ساخته شده. مثلاً Employee ممکن است توضیحات پنج ارزیابی قبلی را ذخیره کند — این اطلاعات گروه تکراری است که باید در جدول Employee-Evaluation split شود.

مبادلات بالقوه

وقتی جدولی را که (فکر می‌کنید) برای اهداف مختلف استفاده می‌شود split می‌کنید، خطر این را دارید که در واقع باید از جداول جدید برای همان کارها استفاده کنید؛ در آن صورت متوجه می‌شوید باید Merge Tables (ادغام جداول) اعمال کنید. استفاده از داده باید تعیین کند آیا باید split شود.

مکانیک‌های به‌روزرسانی طرحواره

برای Split Table ابتدا جدول(ها) را با دستور SQL CREATE TABLE اضافه کنید. این گام اختیاری است چون شاید بتوانید از جدول(های) موجود برای انتقال ستون‌ها استفاده کنید — در آن صورت باید بازآرایی Move Column (انتقال ستون) را اعمال کنید. دوم، trigger لازم است تا ستون‌ها با یکدیگر همگام بمانند. trigger باید با هر تغییر در جداول فراخوانی شود. trigger را طوری پیاده کنید که چرخه ایجاد نشود.

شکل ۶.۱۸. تقسیم جدول Address.

شکل ۶.۱۸ مثالی است که جدول Address در ابتدا اطلاعات آدرس را همراه با نام ایالت ذخیره می‌کند. برای کاهش تکرار داده، Address را به Address و State split کردیم — بازآرایی فعلی طراحی جدول. triggerهای SynchronizeWithAddress و SynchronizeWithState را برای همگام نگه‌داشتن مقادیر در جداول معرفی کردیم:

sql
CREATE TABLE State (
   StateCode VARCHAR(2) NOT NULL,
   Name VARCHAR(40) NOT NULL,
   CONSTRAINT PKState
     PRIMARY KEY (StateCode)
);

COMMENT ON State.Name 'Added as the result of splitting Address into Address and State
 drop date
= December 14 2007';

-- Trigger to keep all the split tables in sync
CREATE OR REPLACE TRIGGER SynchronizeWithAddress
BEFORE INSERT OR UPDATE
ON Address
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
  IF updating THEN
    FindOrCreateState;
  END IF;
  IF inserting THEN
    CreateState;
  END IF;
END;
/

CREATE OR REPLACE TRIGGER SynchronizeWithState
BEFORE UPDATE OF Statename

ON State
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
BEGIN
  IF updating THEN
     FindAndUpdateAllAddressesForStateName;
  END IF;
END;
/

-- On December 14 2007
ALTER TABLE Address DROP COLUMN StateName;
DROP TRIGGER SynchronizeWithAddress;
DROP TRIGGER SynchronizeWithState;

مکانیک‌های مهاجرت داده

باید همهٔ داده از ستون(های) اصلی به ستون‌های جدول جدید کپی شود. در مورد شکل ۶.۱۸، از Address.StateCode و Address.StateName به State.StateCode و State.Name. کد زیر مهاجرت اولیهٔ این داده را نشان می‌دهد:

sql
/*One-time migration of data from Address.StateCode and Address.StateName to State. When both set
of columns are active, there is a need to have a trigger that keeps both set of columns in
 sync
*/

INSERT INTO State (StateCode, Name)
  SELECT StateCode,StateName FROM Address
  WHERE StateCode IS NOT NULL AND StateName IS NOT NULL
  GROUP BY StateCode, StateName;

مکانیک‌های به‌روزرسانی برنامه‌های دسترسی

باید برنامه‌های دسترسی را کامل بررسی و در دورهٔ گذار به‌روز کنید. علاوه بر نیاز واضح کار با ستون‌های جدید به‌جای ستون(های) قبلی، به‌روزرسانی‌های بالقوه:

  1. متادیتای جدول جدید را معرفی کنید. اگر از framework persistence مبتنی بر متادیتا استفاده می‌کنید، باید متادیتا برای State معرفی و متادیتای Address را تغییر دهید.

  2. کد SQL را به‌روز کنید. به‌همین شکل هر SQL تعبیه‌شده‌ای که به Address دسترسی دارد باید برای join با State در صورت لزوم به‌روز شود. این ممکن است عملکرد کد را کمی کاهش دهد.

  3. رابط کاربری را بازآرایی کنید. پس از split جدول اصلی، لایهٔ presentation — اگر قبلاً نمی‌کرد — باید از دادهٔ جدید استفاده کند.

نگاشت‌های Hibernate زیر نشان می‌دهند چگونه جدول Address را split کردیم و جدول State جدید ساختیم:

xml
//Before mapping
<hibernate-mapping>
<class name="Address" table="ADDRESS">
  <id name="id" column="ADDRESSID">
    <generator class="IdGenerator"/>

  </id>
  <property name="street" />
  <property name="city" />
  <property name="stateCode" />
  <property name="stateName" />
</class>
</hibernate-mapping>
//After mapping

//Address table
<hibernate-mapping>
<class name="Address" table="ADDRESS">
  <id name="id" column="ADDRESSID">
    <generator class="IdGenerator"/>
  </id>
  <property name="street" />
  <property name="city" />
  <many-to-one name="state" class="State"
    column="STATECODE" not-null="true"/>
</class>
</hibernate-mapping>

//State table
<hibernate-mapping>
<class name="State" table="STATE">
  <id name="stateCode" column="stateCode">
    <generator class="assigned"/>
  </id>
  <property name="stateName" />
</class>
</hibernate-mapping>