ADO.NET 2.0 - 读者询问能否使用 SqlBulkCopy 对象来大量复制文字文件
我们曾经在「Visual Basic 2005 档案 IO 与数据存取秘诀」一书的第 10 章说明如何使用 SqlBulkCopy 对象来执行大量复制作业。有读者询问,是否可以使用 SqlBulkCopy 对象将文字文件的内容大量复制到数据库。答案当然是肯定的?事实上,只要您能够将数据加载至一个 DataTable 对象或是利用 IDataReader 对象来加以读取,就可以使用 SqlBulkCopy 将其复制到 SQL Server 数据库内的数据表(Table)或检视表(View)当中。
不过我们亲爱的读者在实际测试时遇到了一个问题,那就是他的文字文件内含 200 万笔数据列,因此在将文字文件复制到一个中介用的 DataTable 对象时便发生内存不足的情况?他询问我,该如何解决。
首先我要声明,要将一个内含 200 万笔数据列的文字文件大量复制到 SQL Server 数据库,想必不是一个经常性的作业,像这样的工作,其实可以考虑使用 SQL Server 2005 本身的公用程序与接口工具来完成,不一定非得在前端程序使用 SqlBulkCopy 对象不可。不过既然读者问了,我就试作了一次,结果令人满意。
我的作法是,先剖析文字文件的内容以便正确地将数据列一笔接着一笔写入一个中介用的 DataTable 物件中,等到写满 50000 笔数据记录之后,就使用 SqlBulkCopy 对象将 DataTable 中的 50000 笔数据记录大量复制到 SQL Server 数据库,接着将 DataTable 对象中的所有数据记录清除,然后再继续从文字文件将后续的数据列读入DataTable对象中。反复依此进行,直到已大量复制完所有的数据记录为止。这样一批接着一批读取并大量复制的方式,可以避免发生内存不足的情况。
图表 1 所示者是我所撰写之大量复制程序的接口。我的来源文字文件共内含 1,485,953 笔资料列(将近一百五十万笔),文字文件的档案大小达到 260MB,希望藉由这样的大量数据实作,来验证程序写法的正确性。至于程序代码完整列示如下:
Option Strict On
' 汇入命名空间。
Imports Microsoft.VisualBasic.FileIO
Imports System.Data.SqlTypes
Imports System.Data.SqlClient
Public Class Form1
Private currentRow As String()
Private myRowCount As Integer = 1
Private myBatchCount As Integer = 1
Private myCopiedRows As Long = 0
Private countStart As Long
' 建立「章立民研究室」数据表,此处是当作一个中介数据表来使用。
Private myTable As New DataTable("章立民工作室")
Private Sub btnParseTextFiles_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnGoBulkCopy.Click
Me.btnGoBulkCopy.Enabled = False
' 建立「员工编号」字段。
Dim colEmployeeId As DataColumn = _
myTable.Columns.Add("员工编号", Type.GetType("System.Int32"))
' 建立「身份证字号」字段。
myTable.Columns.Add("身份证字号", Type.GetType("System.String"))
myTable.Columns("身份证字号").MaxLength = 10
myTable.Columns("身份证字号").AllowDBNull = False
' 建立「姓名」字段。
myTable.Columns.Add("姓名", Type.GetType("System.String"))
myTable.Columns("姓名").MaxLength = 12
' 建立「性别」字段。
myTable.Columns.Add("性别", Type.GetType("System.String"))
'myTable.Columns("性别").MaxLength = 1
' 建立「地址」字段。
myTable.Columns.Add("地址", Type.GetType("System.String"))
myTable.Columns("地址").MaxLength = 41
' 建立「邮政编码」字段。
myTable.Columns.Add("邮政编码", Type.GetType("System.String"))
myTable.Columns("邮政编码").MaxLength = 5
' 建立「出生日期」字段。
myTable.Columns.Add("出生日期", Type.GetType("System.DateTime"))
' 建立「婚姻状况」字段。
myTable.Columns.Add("婚姻状况", Type.GetType("System.String"))
' 建立「雇用日期」字段。
myTable.Columns.Add("雇用日期", Type.GetType("System.DateTime"))
' 建立「起薪」字段。
myTable.Columns.Add("起薪", Type.GetType("System.Double"))
' 建立「目前薪资」字段。
myTable.Columns.Add("目前薪资", Type.GetType("System.Double"))
' 建立「加薪日期」字段。
myTable.Columns.Add("加薪日期", Type.GetType("System.DateTime"))
' 建立「部门」字段。
myTable.Columns.Add("部门", Type.GetType("System.String"))
myTable.Columns("部门").MaxLength = 10
Using myReader As New TextFieldParser("Text章立民工作室.txt")
' 表示档案内容是字符分隔。
myReader.TextFieldType = FieldType.Delimited
' 定义文字文件的字符分隔符。
myReader.Delimiters = New String() {","}
' 循环处理文字文件中所有数据列的所有字段。
While Not myReader.EndOfData
Try
currentRow = myReader.ReadFields()
' 略过标题列
If myRowCount > 1 Then
myTable.Rows.Add(currentRow)
End If
Catch ex As MalformedLineException
MessageBox.Show(ex.Message)
Exit Sub
End Try
myRowCount += 1
Me.lblBeingCopyedTextRows.Text = myTable.Rows.Count.ToString
Me.lblBeingCopyedTextRows.Refresh()
If myTable.Rows.Count = 50000 Then
Try
GoBulkCopy()
Catch ex As Exception
MessageBox.Show(ex.Message)
Exit Sub
End Try
' 清空资料表。
myTable.Rows.Clear()
myBatchCount += 1
End If
End While
' 复制最后一批不足50000 笔的数据记录。
If myTable.Rows.Count > 0 Then
GoBulkCopy()
End If
End Using
Me.lblBeingCopyedTextRows.Text = myTable.Rows.Count.ToString
Me.lblTextFileRowCount.Text = _
"来源文字文件的数据笔数:" & (myRowCount - 2).ToString
Me.btnGoBulkCopy.Enabled = True
End Sub
Private Sub GoBulkCopy()
' 利用SqlConnectionStringBuilder 对象来构建连接字符串。
' 由于本范例是在同一个SQL Server 数据库的不同数据表之间进行大量复制作业,
' 因此连接至来源数据库与连接至目标服务器的连接字符串是相同的。
Dim sqlconStringBuilder As New SqlConnectionStringBuilder()
sqlconStringBuilder.DataSource = "(local)SQLExpress"
sqlconStringBuilder.InitialCatalog = "北风贸易"
sqlconStringBuilder.IntegratedSecurity = True
' 建立连结至目标SQL Server 数据库的连接。
Using con_bulkcopy As New _
SqlConnection(sqlconStringBuilder.ConnectionString)
' 开启连接至目标SQL Server 的连接。
con_bulkcopy.Open()
Dim cmdRowCount As New SqlCommand( _
"SELECT COUNT(*) FROM dbo.Bulk_Target_章立民工作室;", _
con_bulkcopy)
If myBatchCount = 1 Then
' 计算出目标数据表在执行大量复制作业前有多少笔数据记录。
countStart = System.Convert.ToInt32(cmdRowCount.ExecuteScalar())
Me.lblRowsCountBeforeBulkCopy.Text = _
"目标数据表在大量复制前拥有的数据笔数= " & countStart.ToString
Me.lblRowsCountBeforeBulkCopy.Refresh()
End If
' 建立一个SqlBulkCopy 对象以便执行大量复制作业。
Using bcp As SqlBulkCopy = New SqlBulkCopy(con_bulkcopy)
' 指定目标数据表的名称。
bcp.DestinationTableName = "dbo.Bulk_Target_章立民工作室"
' 如果来源数据表与目标数据表的各个字段顺序没有完全对应,
' 必须在此设定来源字段与目标字段的对应关系。
' 将来源数据写入目标数据表。
bcp.WriteToServer(myTable)
End Using
' 最后再计算出大量复制了多少笔数据记录。
Dim countEnd As Long = _
System.Convert.ToInt32(cmdRowCount.ExecuteScalar())
' 计算出累计复制笔数。
myCopiedRows = countEnd – countStart
' 显示出批次与大量复制累计笔数。
Me.DataGridView1.Rows.Add( _
New String() {CStr(myBatchCount), CStr(myCopiedRows)})
Me.DataGridView1.Refresh()
End Using
End Sub
End Class
后记:
说真的,这个程序花不到我半小时,但是制作仿真的文字文件并等待程序执行,倒是花了我不少时间。不过,能解决使用者的问题,心中的大石头总算落了地。最近又要开始赶下一本书,全体成员有得忙了唷。
章立民研究室敬上
期待更多精彩,敬请关注:
http://www.china-pub.com/static/jsj_zlm_060824.html