INSERT INTO 查询不能包含多值字段
Posted
技术标签:
【中文标题】INSERT INTO 查询不能包含多值字段【英文标题】:An INSERT INTO query cannot contain a multi-valued field 【发布时间】:2017-08-04 01:37:28 【问题描述】:我想与您分享我的代码...以下是我要开发的简短说明:我创建了一个与我的数据库 (Access Db) 连接的表单。通过表单当我想添加数据时,我将按下一个按钮,新表单将显示一组控件,我的数据将被添加到哪里。然后我读取这些数据并向绑定到 datagridview 的表中添加一个新数据行。直到这里一切都很完美。
但是当我尝试更新我的数据集以保存更改时,它给了我一个错误:“”。
我正在尝试解决问题,但我无法成功:
Private Function AddDataFromControlArrayToDataRow(dataset As DataSet, tableName As String, Ctrl As Control())
Dim row As DataRow = dataset.Tables(tableName).NewRow()
Dim c As Integer = Ctrl.Length - 1
For k As Integer = 0 To c - 1
If (Ctrl(k).GetType.ToString = "System.Windows.Forms.CheckBox") Then
Dim CheckBox As CheckBox = Ctrl(k)
row(k) = CheckBox.Checked
Else
If Not Ctrl(k).Text = String.Empty Then
row(k) = Convert.ChangeType(Ctrl(k).Text, dataset.Tables(tableName).Columns(k).DataType)
End If
End If
Next
dataset.Tables(tableName).Rows.Add(row) '<----up ot here Code is running without error
Form1.TableAdapterManager.UpdateAll(dataset)
Return Nothing
End Function
【问题讨论】:
您的数据库表是否包含附件字段?附件字段是多值字段的一个示例。此类字段需要特殊处理。 我建议您在 Access DB 中显示创建的查询和表结构。执行 INSERT(使用[column_name].Value
)时,多值列需要追加查询。如果可能,也尝试重新创建没有多值列的表。
正如建议的那样,ADO.NET 无法真正处理 Access Attachment
列。它可以检索它们,但是您需要做一些额外的工作才能将数据取出,并且根本无法保存它们。如果你真的必须使用Attachment
列,那么你基本上需要使用DAO 来处理它们。如果您不是特别需要使用 Attachment
列,则不要。 OLE Object
列可以存储二进制数据,您可以使用 ADO.NET 将其保存为 Byte
数组。
是多值文本字段还是附件类型?
我有一个附件字段。
【参考方案1】:
您在 cmets 中提到您在使用 DAO 时遇到问题。您需要添加与此图类似的 Access Engine 的引用(您的版本号可能不同):
然后将此Imports
语句添加到您的代码中:
Imports DAO = Microsoft.Office.Interop.Access.Dao
可以在这个 SO 问题Storing an image into an Attachment field in an Access database 中找到使用 DAO 操作附件字段的示例。
如果您不介意使用 hack,您可以使用 OleDB 添加/删除/提取附件。以下实用程序课程是我大约六年前在 MSDN 论坛上参加的黑客会议的结果。我发现您从AttachFieldName.FileData
字段中检索到的Byte
数组是一个以标题为前缀的数组。前 4 个字节定义了实际文件数据的整数偏移量。这允许您重新创建附加文件。我还发现,如果您在文件数据前加上 8 个字节(前四个也是文件数据的偏移量,其余四个保留为零),那么 Access 将接受它进行存储。它将使用对您不重要的其他值扩展此标头,并使用新的偏移量重写前 4 个字节。我知道这种技术适用于 Access 2007 和 2010,但我尚未在较新版本上进行测试。另外,据我所知,您仍然需要使用DAO中的数据偏移量来提取原始文件;由于Field2.LoadFromFile
命令,加载附件更容易。
我为这个代码转储道歉,但是正确设置命令有点痛苦。因此我写了这个库来简化事情。
Imports System.Data.OleDb
Public NotInheritable Class Attachments
Private Const prependLength As Int32 = 8
''' <summary>Expands the attachment field name to the 2 fields that need to be retrieved</summary>
''' <param name="attachmentFieldName"></param>
''' <returns>"attachmentFieldName.FileData, attachmentFieldName.FileName"</returns>
Public Shared Function ExpandAttachmentFieldNames(attachmentFieldName As String, tableName As String) As String
Return String.Format("[0].[1].FileData, [0].[1].FileName", tableName, attachmentFieldName)
End Function
''' <summary>Prepends the 8 byte header block required by Access to store the bytes that comprise the fiel</summary>
''' <param name="source"></param>
''' <returns>8 bytes + source</returns>
Public Shared Function PrependFileDataHeader(source As Byte()) As Byte()
' Through reverse engineering, it has been determined that while length of the
' actual data header varies based on file type.
' This header always begins with the first 4 bytes representing the offset to the actual data bytes.
' The bytes from index 4 (zero based) to the beginning of the data appear to represent the file type
' and other undetermined information.
' If an 8 byte field in prepended to the data bytes, with the first 4 bytes representing the
' length of 8, it has been found that that Access will accept it. The second 4 bytes will be expanded
' by Access and filled by it. The initial four bytes is modified to the new data offset.
Dim ret As Byte() = New Byte(0 To (prependLength + source.Length) - 1)
Dim lengthBytes As Byte() = BitConverter.GetBytes(prependLength)
Array.ConstrainedCopy(lengthBytes, 0, ret, 0, lengthBytes.Length)
Array.ConstrainedCopy(source, 0, ret, prependLength, source.Length)
Return ret
End Function
''' <summary>Extracts the bytes that define the attached file</summary>
''' <param name="fileData">the header prepended bytes array returned by Access</param>
''' <returns>the bytes defining the attached file</returns>
Public Shared Function ExtractFileBytes(fileData As Byte()) As Byte()
Dim dataOffset As Int32 = BitConverter.ToInt32(fileData, 0)
Dim databytes(0 To (fileData.Length - dataOffset) - 1) As Byte
Array.ConstrainedCopy(fileData, dataOffset, databytes, 0, fileData.Length - dataOffset)
Return databytes
End Function
''' <summary>
''' Takes an Access FileData byte array and returns a stream of it contained file.
''' </summary>
''' <param name="fileData">the attachment data as received from Access</param>
''' <returns>MemoryStream constrained to the contained file data</returns>
Public Shared Function FileDataAsStream(fileData As Byte()) As IO.MemoryStream
Dim dataOffset As Int32 = BitConverter.ToInt32(fileData, 0)
Dim datalength As Int32 = fileData.Length - dataOffset
Return New IO.MemoryStream(fileData, dataOffset, datalength)
End Function
''' <summary>
''' Takes FileData Byte() from Access and writes its contained file to the passed stream.
''' </summary>
''' <param name="fileData">the attachment data as received from Access</param>
''' <param name="strm">stream to copy file contents to</param>
''' <remarks></remarks>
Public Shared Sub WriteFileDataToStream(fileData As Byte(), strm As IO.Stream)
Dim dataOffset As Int32 = BitConverter.ToInt32(fileData, 0)
Dim datalength As Int32 = fileData.Length - dataOffset
strm.Write(fileData, dataOffset, datalength)
End Sub
''' <summary>
''' Copies entire stream to an Access FileData Byte()
''' </summary>
''' <param name="strm">source stream to wrap in Access FileData Byte()</param>
''' <returns>An Access FileData Byte()</returns>
''' <remarks></remarks>
Public Shared Function FileDataFromStream(strm As IO.Stream) As Byte()
Dim ret As Byte() = Nothing
If strm.CanSeek Then
Dim dataLength As Int32 = CInt(strm.Length)
strm.Position = 0
ret = New Byte(0 To (prependLength + dataLength) - 1)
Dim lengthBytes As Byte() = BitConverter.GetBytes(prependLength)
Array.ConstrainedCopy(lengthBytes, 0, ret, 0, lengthBytes.Length)
strm.Read(ret, prependLength, dataLength)
Else
Throw New IO.IOException("Stream must be seekable.")
End If
Return ret
End Function
''' <summary>
''' Copies data from read file to an Access FileData Byte()
''' </summary>
''' <param name="filePath">Full path to source file</param>
''' <returns>An Access FileData Byte()</returns>
Public Shared Function FileDataLoadFile(filePath As String) As Byte()
Dim ret As Byte() = Nothing
Dim fi As New IO.FileInfo(filePath)
If fi.Exists Then
Dim strm As IO.Stream = IO.File.OpenRead(filePath)
ret = FileDataFromStream(strm)
Else
Throw New IO.FileNotFoundException(filePath)
End If
Return ret
End Function
''' <summary>
''' Prepares a OleDBCommand with parameters to add an attachment to record
''' </summary>
''' <param name="connection">OleDbConnection to use</param>
''' <param name="attachmentFieldname">Name of attachment field</param>
''' <param name="tableName">DB Table name</param>
''' <param name="attachmentFileData">the raw bytes that comprise the file to attach</param>
''' <param name="attachmentFileName">a name for this attachment</param>
''' <param name="pkName">the primary key field name</param>
''' <param name="pkType">the type of the primary key, typically OleDbType.Integer</param>
''' <param name="pkValue">primary key value of the racord to add attachment to</param>
''' <returns>prepared OleDbCommand</returns>
Public Shared Function AddAttachmentCommand(connection As OleDbConnection,
attachmentFieldname As String,
tableName As String,
attachmentFileData As Byte(),
attachmentFileName As String,
pkName As String,
pkType As OleDbType,
pkValue As Object) As OleDbCommand
Dim insertCommand As New OleDb.OleDbCommand()
insertCommand.CommandText = String.Format("INSERT INTO [0] ([0].[1].FileData, [0].[1].FileName) VALUES (?, ?) WHERE ([0].[2]=?)", tableName, attachmentFieldname, pkName)
insertCommand.Connection = connection
' Parameter Order: FileData, FileName, pk
Dim paramData As New OleDbParameter("FileData", OleDbType.Binary)
paramData.Value = PrependFileDataHeader(attachmentFileData)
insertCommand.Parameters.Add(paramData)
Dim paramName As New OleDbParameter("FileName", OleDbType.WChar)
paramName.Value = attachmentFileName
insertCommand.Parameters.Add(paramName)
insertCommand.Parameters.Add(pkName, pkType).Value = pkValue
Return insertCommand
End Function
''' <summary>
''' Prepares an OleDBCommand that removes the specified attachment from a record.
''' </summary>
''' <param name="connection">OleDbConnection to use</param>
''' <param name="attachmentFieldname">Name of attachment field</param>
''' <param name="tableName">DB Table name</param>
''' <param name="attachmentFileName">the attachment name as received from Access</param>
''' <param name="pkName">the primary key field name</param>
''' <param name="pkType">the type of the primary key, typically OleDbType.Integer</param>
''' <param name="pkValue">primary key value of the racord to delete attachment to</param>
''' <returns>prepared OleDbCommand</returns>
Public Shared Function DeleteAttachmentCommand(connection As OleDbConnection,
attachmentFieldname As String,
tableName As String,
attachmentFileName As String,
attachmentFileData As Byte(),
pkName As String,
pkType As OleDbType,
pkValue As Object) As OleDbCommand
' Note: Eventhough Access acts like FileName is the pk for attachments,
' we need to include filedata in contraints, orelse all attachments for record are deleted.
Dim deleteCommand As New OleDbCommand()
deleteCommand.CommandText =
String.Format("DELETE [0].[1].FileData From [0] WHERE ( ([0].[2] = ?) AND ([0].[1].FileName = ?) AND ([0].[1].FileData = ?) )",
tableName, attachmentFieldname, pkName)
deleteCommand.Connection = connection
' Parameter Order: pk, FileName
Dim paramPK As OleDbParameter = deleteCommand.Parameters.Add(pkName, pkType)
paramPK.Value = pkValue
Dim paramName As New OleDbParameter("FileName", OleDbType.WChar)
paramName.Value = attachmentFileName
deleteCommand.Parameters.Add(paramName)
Dim paramData As New OleDbParameter("FileData", OleDbType.Binary)
paramData.Value = attachmentFileData
deleteCommand.Parameters.Add(paramData)
Return deleteCommand
End Function
End Class ' Attachments
Public NotInheritable Class Connect
Public Shared Function ConnectionString(dbFilePath As String) As String
Return String.Format("Provider=Microsoft.ACE.OLEDB.12.0;Data Source='0';Persist Security Info=True", dbFilePath)
End Function
End Class
出于下面显示的基于您的表架构的演示使用代码的目的,上述类位于命名空间AccessUtilities
中。 Button
处理程序添加新记录和附件。 Button2
处理程序演示如何删除附件。我从来没有弄清楚如何修改(更新)附件。如果您需要这样做,只需删除现有的并使用修改后的信息添加一个新的。
' Table Schema
' Field Type
' -------------- --------------------
' ID AutoNumber
' ClientName Text
' Attachments Attachment
Public Class Form1
Const pkName As String = "ID"
Const pkDataType As OleDb.OleDbType = OleDb.OleDbType.Integer
Const attachmentFieldName As String = "Attachments"
Const tableName As String = "MyTable"
Const dbPath As String = "C:\Users\UserName\Documents\DBs\Access\AttachmentDemoTest.accdb"
' adds a empty record and returns its PK (ID)
Private Function AddNewRecord(clientName As String) As Int32
Dim ret As Int32
Using conn As New OleDb.OleDbConnection(AccessUtilities.Connect.ConnectionString(dbPath))
Dim insertCommand As New OleDb.OleDbCommand()
insertCommand.CommandText = String.Format("INSERT INTO 0 (ClientName) VALUES (?)", tableName)
Dim paramDesc As OleDb.OleDbParameter = insertCommand.Parameters.Add("Description", OleDb.OleDbType.VarWChar)
paramDesc.IsNullable = True
paramDesc.Value = DBNull.Value
insertCommand.Connection = conn
conn.Open()
ret = insertCommand.ExecuteNonQuery
If ret = 1 Then ' one record inserted, retrieve pk
insertCommand.CommandText = "Select @@IDENTITY"
insertCommand.Parameters.Clear()
ret = CInt(insertCommand.ExecuteScalar)
End If
conn.Close()
End Using
Return ret
End Function
Private Sub AddRecordAndAttachment(clientName As String, fileBytes As Byte(), fileName As String)
Dim id As Int32 = AddNewRecord(clientName)
' adding an attachment only needs a way to refer to the record to attach to
' this is done using the pk (ID)
Using conn As New OleDb.OleDbConnection(AccessUtilities.Connect.ConnectionString(dbPath))
Dim attachCommand As OleDb.OleDbCommand = AccessUtilities.Attachments.AddAttachmentCommand(conn, attachmentFieldName, tableName, fileBytes, fileName, pkName, pkDataType, id)
conn.Open()
Dim result As Int32 = attachCommand.ExecuteNonQuery()
conn.Close()
End Using
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' demo adding new record and attachment
Dim fileBytes As Byte() = IO.File.ReadAllBytes("C:\Users\UserName\Pictures\BottleTop.png")
AddRecordAndAttachment("Fred Inc.", fileBytes, "FredsPic.png")
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Dim id As Int32 = 1 ' assumed - change as needed
' need to retrieve FileData and FileName from an attachment
Using conn As New OleDb.OleDbConnection(AccessUtilities.Connect.ConnectionString(dbPath))
' retrieve all attachments for id
Dim selectCommand As New OleDb.OleDbCommand
selectCommand.CommandText = String.Format("Select 0 From [1] Where [1].[2]=?", AccessUtilities.Attachments.ExpandAttachmentFieldNames(attachmentFieldName, tableName), tableName, pkName)
selectCommand.Parameters.Add("ID", OleDb.OleDbType.Integer).Value = id
selectCommand.Connection = conn
Dim dt As New DataTable
Dim da As New OleDb.OleDbDataAdapter(selectCommand)
da.Fill(dt)
If dt.Rows.Count > 0 Then ' we have attachments
' delete the 1st attachment retrieved
Dim accessFileData As Byte() = CType(dt.Rows(0).Item(String.Format("0.1.FileData", tableName, attachmentFieldName)), Byte())
Dim accessFileName As String = CStr(dt.Rows(0).Item(String.Format("0.1.FileName", tableName, attachmentFieldName)))
Dim deleteCommand As OleDb.OleDbCommand = AccessUtilities.Attachments.DeleteAttachmentCommand(conn, attachmentFieldName, tableName, accessFileName, accessFileData, pkName, pkDataType, id)
conn.Open()
Dim result As Int32 = deleteCommand.ExecuteNonQuery() ' if = 1 then attachment deleted
conn.Close()
End If
End Using
End Sub
End Class
您也可以找到以下感兴趣的文章。
Using multivalued fields in queries
【讨论】:
以上是关于INSERT INTO 查询不能包含多值字段的主要内容,如果未能解决你的问题,请参考以下文章
access不允许在select into 语句中使用多值字段
ACCESS中提示“不允许在select into语句中使用多值字段”