同様にデータベースを設計しました (INSERT のみで、UPDATE も DELETE もありません)。
ほとんどすべての SELECT クエリは、各テーブルの現在の行 (最も高いリビジョン番号) のみのビューに対して行われました。
景色はこんな感じでした…
SELECT
dbo.tblBook.BookId,
dbo.tblBook.RevisionId,
dbo.tblBook.Title,
dbo.tblBook.AuthorId,
dbo.tblBook.Price,
dbo.tblBook.Deleted
FROM
dbo.tblBook INNER JOIN
(
SELECT
BookId,
MAX(RevisionId) AS RevisionId
FROM
dbo.tblBook
GROUP BY
BookId
) AS CurrentBookRevision ON
dbo.tblBook.BookId = CurrentBookRevision.BookId AND
dbo.tblBook.RevisionId = CurrentBookRevision.RevisionId
WHERE
dbo.tblBook.Deleted = 0
また、挿入 (および更新と削除) はすべてストアド プロシージャ (テーブルごとに 1 つ) によって処理されました。
ストアドプロシージャは次のように見えました…
ALTER procedure [dbo].[sp_Book_CreateUpdateDelete]
@BookId uniqueidentifier,
@RevisionId bigint,
@Title varchar(256),
@AuthorId uniqueidentifier,
@Price smallmoney,
@Deleted bit
as
insert into tblBook
(
BookId,
RevisionId,
Title,
AuthorId,
Price,
Deleted
)
values
(
@BookId,
@RevisionId,
@Title,
@AuthorId,
@Price,
@Deleted
)
Visual Basic コードでは、トランザクションごとにリビジョン番号が処理されていました…
Shared Sub Save(ByVal UserId As Guid, ByVal Explanation As String, ByVal Commands As Collections.Generic.Queue(Of SqlCommand))
Dim Connection As SqlConnection = New SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings("Connection").ConnectionString)
Connection.Open()
Dim Transaction As SqlTransaction = Connection.BeginTransaction
Try
Dim RevisionId As Integer = Nothing
Dim RevisionCommand As SqlCommand = New SqlCommand("sp_Revision_Create", Connection)
RevisionCommand.CommandType = CommandType.StoredProcedure
RevisionCommand.Parameters.AddWithValue("@RevisionId", 0)
RevisionCommand.Parameters(0).SqlDbType = SqlDbType.BigInt
RevisionCommand.Parameters(0).Direction = ParameterDirection.Output
RevisionCommand.Parameters.AddWithValue("@UserId", UserId)
RevisionCommand.Parameters.AddWithValue("@Explanation", Explanation)
RevisionCommand.Transaction = Transaction
LogDatabaseActivity(RevisionCommand)
If RevisionCommand.ExecuteNonQuery() = 1 Then 'rows inserted
RevisionId = CInt(RevisionCommand.Parameters(0).Value) 'generated key
Else
Throw New Exception("Zero rows affected.")
End If
For Each Command As SqlCommand In Commands
Command.Connection = Connection
Command.Transaction = Transaction
Command.CommandType = CommandType.StoredProcedure
Command.Parameters.AddWithValue("@RevisionId", RevisionId)
LogDatabaseActivity(Command)
If Command.ExecuteNonQuery() < 1 Then 'rows inserted
Throw New Exception("Zero rows affected.")
End If
Next
Transaction.Commit()
Catch ex As Exception
Transaction.Rollback()
Throw New Exception("Rolled back transaction", ex)
Finally
Connection.Close()
End Try
End Sub
各テーブルのオブジェクトを作成し、それぞれにコンストラクター、インスタンスのプロパティとメソッド、create-update-delete コマンド、一連のファインダー関数、および IComparable 並べ替え関数を含めました。膨大な量のコードでした。
1 対 1 の DB テーブルから VB オブジェクトへ...
Public Class Book
Implements iComparable
#Region " Constructors "
Private _BookId As Guid
Private _RevisionId As Integer
Private _Title As String
Private _AuthorId As Guid
Private _Price As Decimal
Private _Deleted As Boolean
...
Sub New(ByVal BookRow As DataRow)
Try
_BookId = New Guid(BookRow("BookId").ToString)
_RevisionId = CInt(BookRow("RevisionId"))
_Title = CStr(BookRow("Title"))
_AuthorId = New Guid(BookRow("AuthorId").ToString)
_Price = CDec(BookRow("Price"))
Catch ex As Exception
'TO DO: log exception
Throw New Exception("DataRow does not contain valid Book data.", ex)
End Try
End Sub
#End Region
...
#Region " Create, Update & Delete "
Function Save() As SqlCommand
If _BookId = Guid.Empty Then
_BookId = Guid.NewGuid()
End If
Dim Command As SqlCommand = New SqlCommand("sp_Book_CreateUpdateDelete")
Command.Parameters.AddWithValue("@BookId", _BookId)
Command.Parameters.AddWithValue("@Title", _Title)
Command.Parameters.AddWithValue("@AuthorId", _AuthorId)
Command.Parameters.AddWithValue("@Price", _Price)
Command.Parameters.AddWithValue("@Deleted", _Deleted)
Return Command
End Function
Shared Function Delete(ByVal BookId As Guid) As SqlCommand
Dim Doomed As Book = FindByBookId(BookId)
Doomed.Deleted = True
Return Doomed.Save()
End Function
...
#End Region
...
#Region " Finders "
Shared Function FindByBookId(ByVal BookId As Guid, Optional ByVal TryDeleted As Boolean = False) As Book
Dim Command As SqlCommand
If TryDeleted Then
Command = New SqlCommand("sp_Book_FindByBookIdTryDeleted")
Else
Command = New SqlCommand("sp_Book_FindByBookId")
End If
Command.Parameters.AddWithValue("@BookId", BookId)
If Database.Find(Command).Rows.Count > 0 Then
Return New Book(Database.Find(Command).Rows(0))
Else
Return Nothing
End If
End Function
このようなシステムでは、各行の過去のバージョンがすべて保持されますが、管理が非常に困難になる可能性があります。
長所:
- 保持される履歴の合計
- ストアド プロシージャの削減
短所:
- データの整合性をデータベース以外のアプリケーションに依存する
- 膨大な量のコードを書く
- データベース内で外部キーを管理しない (さようなら、Linq から SQL への自動オブジェクト生成)
- 保存されている過去のバージョン管理をすべて取得するための優れたユーザー インターフェイスはまだ思いつきません。
結論:
- すぐに使える使いやすい ORM ソリューションがなければ、新しいプロジェクトでこのようなトラブルに遭遇することはありません。
Microsoft Entity Framework がこのようなデータベース設計をうまく処理できるかどうか、興味があります。
ジェフと残りのスタック オーバーフロー チームは、スタック オーバーフローの開発中に同様の問題に対処しなければならなかったに違いありません。編集された質問と回答の過去のリビジョンは保存され、取得可能です。
Jeff は、彼のチームが Linq to SQL と MS SQL Server を使用したと述べていると思います。
彼らはこれらの問題をどのように処理したのだろうか。