主子表最常見的大概就是用在進銷存、MRP、ERP裡面,比如一張銷售訂單,訂單Order(ID,OrderDate),訂單明細OrderDetail(OrderID, ProductID, Num,Price)這個大概就是最簡單的主子表了,兩個表通過ID與OrderID建立關聯,這裡主鍵ID是自增的INT類型,OrderID是表OrderDetail的外鍵。當然,鍵的選擇方法很多,現在我們選擇的是在sql裡面最簡單的方法。
對於這樣的表結構,我們最常見的問題就是保存的時候怎樣處理鍵值的問題,因為兩個表關聯非常的緊密,我們進行保存的時候需要把它們放在一個事務裡面,這時問題就會出現,Order表中的ID是自動增長型的字段。現在需要我們錄入一張訂單,包括在Order表中插入一條記錄以及在OrderDetail表中插入若干條記錄。因為Order表中的ID是自動增長型的字段,那麼我們在記錄正式插入到數據庫之前無法事先得知它的取值,只有在更新後才能知道數據庫為它分配的是什麼值,然後再用這個ID作為OrderDetail表的OrderID的值,最後更新OderDetail表。但是,為了確保數據的一致性,Order與OrderDetail在更新時必須在事務保護下同時進行,即確保兩表同時更行成功,這個就會有點困擾。
解決這類問題常見的主要有兩類方法:
一種是微軟在網上書店裡使用的方法,使用了四個存儲過程。改裝一下,使之符合現在的例子
--存儲過程一
CREATE PROCEDURE InsertOrder
@Id INT = NULL OUTPUT,
@OrderDate DATETIME = NULL,
@ProductIDList NVARCHAR(4000) = NULL,
@NUMList NVARCHAR(4000) = NULL,
@PriceList NVARCHAR(4000) = NULL
AS
SET NOCOUNT ON
SET XACT_ABORT ON
BEGIN TRANSACTION
--插入主表
INSERT Orders(OrderDate) select @OrderDate
SELECT @Id = @@IDENTITY
-- 插入子表
IF @ProductIDList IS NOT NULL
EXECUTE InsertOrderDetailsByList @Id, @ProductIdList, @nUMList, @PriceList
COMMIT TRANSACTION
RETURN 0
--存儲過程二
CREATE PROCEDURE InsertOrderDetailsByList
@Id INT,
@ProductIDList NVARCHAR(4000) = NULL,
@NUMList NVARCHAR(4000) = NULL,
@PriceList NVARCHAR(4000) = NULL
AS
SET NOCOUNT ON
DECLARE @Length INT
DECLARE @FirstProductIdWord NVARCHAR(4000)
DECLARE @FirstNumWord NVARCHAR(4000)
DECLARE @FirstPriceWord NVARCHAR(4000)
DECLARE @ProductId INT
DECLARE @Num INT
DECLARE @Price MONEY
SELECT @Length = DATALENGTH(@ProductIDList)
WHILE @Length > 0
BEGIN
EXECUTE @Length = PopFirstWord @@ProductIDList OUTPUT, @FirstProductIdWord OUTPUT
EXECUTE PopFirstWord @NUMList OUTPUT, @FirstNumWord OUTPUT
EXECUTE PopFirstWord @PriceList OUTPUT, @FirstPriceWord OUTPUT
IF @Length > 0
BEGIN
SELECT @ProductId = CONVERT(INT, @FirstProductIdWord)
SELECT @Num = CONVERT(INT, @FirstNumWord)
SELECT @Price = CONVERT(MONEY, @FirstPriceWord)
EXECUTE InsertOrderDetail @Id, @ProductId, @Price, @Num
END
END
--存儲過程三
CREATE PROCEDURE PopFirstWord
@SourceString NVARCHAR(4000) = NULL OUTPUT,
@FirstWord NVARCHAR(4000) = NULL OUTPUT
AS
SET NOCOUNT ON
DECLARE @OldWord NVARCHAR(4000)
DECLARE @Length INT
DECLARE @CommaLocation INT
SELECT @OldWord = @SourceString
IF NOT @OldWord IS NULL
BEGIN
SELECT @CommaLocation = CHARINDEX(',',@OldWord)
SELECT @Length = DATALENGTH(@OldWord)
IF @CommaLocation = 0
BEGIN
SELECT @FirstWord = @OldWord
SELECT @SourceString = NULL
RETURN @Length
END
SELECT @FirstWord = SUBSTRING(@OldWord, 1, @CommaLocation -1)
SELECT @SourceString = SUBSTRING(@OldWord, @CommaLocation + 1, @Length - @CommaLocation)
RETURN @Length - @CommaLocation
END
RETURN 0
------------------------------------------------
--存儲過程四
CREATE PROCEDURE InsertOrderDetail
@OrderId INT = NULL,
@ProductId INT = NULL,
@Price MONEY = NULL,
@Num INT = NULL
AS
SET NOCOUNT ON
INSERT OrderDetail(OrderId,ProductId,Price,Num)
SELECT @OrderId,@ProductId,@Price,@Num
RETURN 0
插入時,傳入的子表數據都是長度為4000的NVARCHAR類型,各個字段使用“,”分割,然後調用PopFirstWord分拆後分別調用InsertOrderDetail進行保存,因為在InsertOrder中進行了事務處理,數據的安全性也比較有保障,幾個存儲過程設計的精巧別致,很有意思,但是子表的幾個數據大小不能超過4000字符,恐怕不大保險。
第二種方法是我比較常用的,為了方便,就不用存儲過程了,這個例子用的是VB.Net。
‘處理數據的類
Public class DbTools
private Const _IDENTITY_SQL As String = "SELECT @@IDENTITY AS ID"
private Const _ID_FOR_REPLACE As
String = "_ID_FOR_REPLACE"‘對主子表插入記錄
Public Function InsFatherSonRec(ByVal main_sql As String, ByVal ParamArray arParam() As String) As Integer
Dim conn As New SqlConnection(StrConn)
Dim ID AS INTEGER
conn.Open()
Dim trans As SqlTransaction = conn.BeginTransaction
Try
'主記錄
myDBTools.SqlData.ExecuteNonQuery(trans, CommandType.Text, main_sql)
'返回新增ID號
ID = myDBTools.SqlData.ExecuteScalar(trans, CommandType.Text, _IDENTITY_SQL)
'從記錄
If Not arParam Is Nothing Then
For Each sql In arParam
'將剛獲得的ID號代入
sql = sql.Replace(_ID_FOR_REPLACE, ID)
myDBTools.SqlData.ExecuteNonQuery(trans, CommandType.Text, sql)
Next
End If
trans.Commit()
Catch e As Exception
trans.Rollback()
Finally
conn.Close()
End Try
Return ID
End Function
End class
上面這段代碼裡有myDBTools,是對常見的數據庫操作封裝後的類,這個類對數據庫進行直接的操作,有經驗的.Net數據庫程序員基本上都會有,一些著名的例子程序一般也都提供。
上面的是通用部分,下面是對具體單據的操作
Publid class Order
Public _OrderDate as date ‘主表記錄
Public ChildDt as datatable ‘子表記錄,結構與OrderDetail一致
Public function Save() as integer
Dim str as string
Dim i as integer
Dim arParam() As String
Dim str as string=”insert into Order(OrderDate) values(‘” & _OrderDate & “’)”
If not Childdt is nothing then
arParam = New String(ChildDT.Rows.Count - 1) {}
for i=0 to Childdt.rows.count-1
arparam(i)= ”insert into OrderDetail(OrderID,ProductID,Num,Price) Values(_ID_FOR_REPLACE,” & drow(“ProductID) & “,” & drow(“Num”) & “,” drow(“price”) & “)”
next i
End if
Return (new dbtools). InsFatherSonRec(str,arparam)
End class
上面的兩個例子為了方便解釋,去掉了一些檢驗驗證過程,有興趣的朋友可以參照網上書店的例子研究第一種方法,或者根據自己的需要對第二種方法進行修改。