将任何文件保存到数据库中,只需将其转换为字节数组?

79

将文件转换为字节数组是将任何文件格式保存到磁盘或数据库二进制列的最佳方式吗?

因此,如果有人想要保存 .gif 或者 .doc/.docx 或者 .pdf 文件,我是否可以将其转换为 UTF8 字节数组并将其作为字节流保存到数据库中呢?


2
显然,将其转换为字节数组意味着将整个内容加载到[虚拟]内存中。 - Steve Smith
8个回答

172

由于没有提到您使用的是哪个数据库,我假设您在使用SQL Server。以下解决方案适用于2005和2008版本。

您需要创建一个表,其中包含VARBINARY(MAX)作为其列之一。在我的示例中,我创建了名为Raporty的表,其中列RaportPlik是一个VARBINARY(MAX)列。

从驱动器将文件存入数据库的方法:

public static void databaseFilePut(string varFilePath) {
    byte[] file;
    using (var stream = new FileStream(varFilePath, FileMode.Open, FileAccess.Read)) {
        using (var reader = new BinaryReader(stream)) {
            file = reader.ReadBytes((int) stream.Length);       
        }          
    }
    using (var varConnection = Locale.sqlConnectOneTime(Locale.sqlDataConnectionDetails))
    using (var sqlWrite = new SqlCommand("INSERT INTO Raporty (RaportPlik) Values(@File)", varConnection)) {
        sqlWrite.Parameters.Add("@File", SqlDbType.VarBinary, file.Length).Value = file;
        sqlWrite.ExecuteNonQuery();
    }
}

这个方法是从数据库获取file并将其保存在drive:

public static void databaseFileRead(string varID, string varPathToNewLocation) {
    using (var varConnection = Locale.sqlConnectOneTime(Locale.sqlDataConnectionDetails))
    using (var sqlQuery = new SqlCommand(@"SELECT [RaportPlik] FROM [dbo].[Raporty] WHERE [RaportID] = @varID", varConnection)) {
        sqlQuery.Parameters.AddWithValue("@varID", varID);
        using (var sqlQueryResult = sqlQuery.ExecuteReader())
            if (sqlQueryResult != null) {
                sqlQueryResult.Read();
                var blob = new Byte[(sqlQueryResult.GetBytes(0, 0, null, 0, int.MaxValue))];
                sqlQueryResult.GetBytes(0, 0, blob, 0, blob.Length);
                using (var fs = new FileStream(varPathToNewLocation, FileMode.Create, FileAccess.Write)) 
                    fs.Write(blob, 0, blob.Length);
            }
    }
}

这个方法是从数据库获取文件,并将其放入MemoryStream中。

public static MemoryStream databaseFileRead(string varID) {
    MemoryStream memoryStream = new MemoryStream();
    using (var varConnection = Locale.sqlConnectOneTime(Locale.sqlDataConnectionDetails))
    using (var sqlQuery = new SqlCommand(@"SELECT [RaportPlik] FROM [dbo].[Raporty] WHERE [RaportID] = @varID", varConnection)) {
        sqlQuery.Parameters.AddWithValue("@varID", varID);
        using (var sqlQueryResult = sqlQuery.ExecuteReader())
            if (sqlQueryResult != null) {
                sqlQueryResult.Read();
                var blob = new Byte[(sqlQueryResult.GetBytes(0, 0, null, 0, int.MaxValue))];
                sqlQueryResult.GetBytes(0, 0, blob, 0, blob.Length);
                //using (var fs = new MemoryStream(memoryStream, FileMode.Create, FileAccess.Write)) {
                memoryStream.Write(blob, 0, blob.Length);
                //}
            }
    }
    return memoryStream;
}

这种方法是将 MemoryStream 存入数据库:

public static int databaseFilePut(MemoryStream fileToPut) {
        int varID = 0;
        byte[] file = fileToPut.ToArray();
        const string preparedCommand = @"
                    INSERT INTO [dbo].[Raporty]
                               ([RaportPlik])
                         VALUES
                               (@File)
                        SELECT [RaportID] FROM [dbo].[Raporty]
            WHERE [RaportID] = SCOPE_IDENTITY()
                    ";
        using (var varConnection = Locale.sqlConnectOneTime(Locale.sqlDataConnectionDetails))
        using (var sqlWrite = new SqlCommand(preparedCommand, varConnection)) {
            sqlWrite.Parameters.Add("@File", SqlDbType.VarBinary, file.Length).Value = file;

            using (var sqlWriteQuery = sqlWrite.ExecuteReader())
                while (sqlWriteQuery != null && sqlWriteQuery.Read()) {
                    varID = sqlWriteQuery["RaportID"] is int ? (int) sqlWriteQuery["RaportID"] : 0;
                }
        }
        return varID;
    }

2
我猜很多人是通过搜索引擎找到这个答案的,对他们有帮助,但它并没有回答问题。楼主问是否有更好的选择,并想进行比较,我猜。 - bugybunny
3
这个答案是在2010年给出的。随时可以添加更多填补空缺的答案。 - MadBoy
1
我收到了一个“当前上下文中不存在'Locale'的名称”的错误。在“using”下面应该添加什么? - Ian
2021年使用VS 2022 - 仍然创造奇迹!谢谢 - WiiLF
1
我建议不要在企业解决方案中这样做,因为同时处理大量大文件时可能会导致内存溢出异常。我建议直接使用Stream,因为这可以避免将整个文件存储在内存中(字节数组)。 请参阅Microsoft的建议: https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sqlclient-streaming-support - Ykok

12

虽然您可以以这种方式存储文件,但它有显著的权衡:

  • 大多数数据库都不针对海量二进制数据进行优化,当表膨胀时,即使使用索引,查询性能经常会急剧下降,(SQL Server 2008,通过FILESTREAM列类型是例外)。
  • 数据库备份/复制变得非常缓慢。
  • 如果2百万张图片的磁盘损坏了,只需替换RAID上的磁盘就可以轻松解决,而损坏数据库表则更难处理。
  • 如果您在文件系统中意外删除了一打图像,则操作人员可以从备份中轻松替换它们,并且由于表索引相对较小,因此可以快速还原。 如果您在巨大的数据库表中意外删除了一打图像,则需要等待很长时间才能从备份中恢复数据库,同时瘫痪整个系统。

这只是我能够想到的一些缺点。 对于微小项目,也许以这种方式存储文件是值得的,但如果您正在设计企业级软件,强烈建议不要这样做。


12
所以你的回答是“不要”。抱歉,但这不是一个回答——特别是如果这可能是他们的要求。提问者正在询问如何最好地完成它,而不是是否应该这样做。 - vapcguy

7

这主要取决于数据库服务器。

例如,SQL Server 2008支持FILESTREAM数据类型来处理这种情况。

除此之外,如果您使用MemoryStream,它有一个ToArray()方法可以将其转换为byte[] - 这可以用于填充varbinary字段。


5
我将描述我在SQL Server和Oracle中存储文件的方式。它主要取决于您首先如何获取文件,以及取决于您使用哪个数据库来存储其内容以及如何存储它。这些是两个不同的数据库示例,我使用了两种不同的获取文件方法。 SQL Server 简短回答:我使用了一个base64字节字符串,将其转换为byte[]并存储在varbinary(max)字段中。
长回答:
假设您通过网站上传,因此您正在使用控件或React DropZone。为了获取文件,您正在执行诸如var myFile = document.getElementById("myFileControl")[0];myFile = this.state.files[0];之类的操作。
从那里开始,我会使用此处的代码获取base64字符串:Convert input=file to byte array(使用函数UploadFile2)。
然后,我会将该字符串、文件名(myFile.name)和类型(myFile.type)放入JSON对象中:
var myJSONObj = {
    file: base64string,
    name: myFile.name,
    type: myFile.type,
}

将文件通过XMLHttpRequest发送到MVC服务器后端,指定Content-Type为application/jsonxhr.send(JSON.stringify(myJSONObj);。您需要构建一个ViewModel来绑定它:

public class MyModel
{
    public string file { get; set; }
    public string title { get; set; }
    public string type { get; set; }
}

并将[FromBody]MyModel myModelObj作为传入参数指定:

[System.Web.Http.HttpPost]  // required to spell it out like this if using ApiController, or it will default to System.Mvc.Http.HttpPost
public virtual ActionResult Post([FromBody]MyModel myModelObj)

然后您可以将此添加到该函数中,并使用Entity Framework保存它:
MY_ATTACHMENT_TABLE_MODEL tblAtchm = new MY_ATTACHMENT_TABLE_MODEL();
tblAtchm.Name = myModelObj.name;
tblAtchm.Type = myModelObj.type;
tblAtchm.File = System.Convert.FromBase64String(myModelObj.file);
EntityFrameworkContextName ef = new EntityFrameworkContextName();
ef.MY_ATTACHMENT_TABLE_MODEL.Add(tblAtchm);
ef.SaveChanges();

tblAtchm.File = System.Convert.FromBase64String(myModelObj.file); 是关键的代码行。

您需要一个模型来表示数据库表:

public class MY_ATTACHMENT_TABLE_MODEL 
{
    [Key]
    public byte[] File { get; set; }  // notice this change
    public string Name { get; set; }
    public string Type { get; set; }
}

这将把数据保存到一个 varbinary(max) 字段中,作为一个 byte[]。其中,NameType 分别是 nvarchar(250)nvarchar(10)。你可以通过在表中添加 int 列和 MY_ATTACHMENT_TABLE_MODEL 中的 public int Size { get; set;},来包含大小,并在上面添加一行代码 tblAtchm.Size = System.Convert.FromBase64String(myModelObj.file).Length;Oracle 简短回答:将其转换为 byte[],将其分配给 OracleParameter,将其添加到您的 OracleCommand 中,并使用参数的 ParameterName 值的引用更新表的 BLOB 字段::BlobParameter
长回答:当我在 Oracle 上进行此操作时,我使用了一个 OpenFileDialog 并通过以下方式检索并发送字节/文件信息:
byte[] array;
OracleParameter param = new OracleParameter();
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
dlg.Filter = "Image Files (*.jpg, *.jpeg, *.jpe)|*.jpg;*.jpeg;*.jpe|Document Files (*.doc, *.docx, *.pdf)|*.doc;*.docx;*.pdf"
if (dlg.ShowDialog().Value == true)
{
    string fileName = dlg.FileName;
    using (FileStream fs = File.OpenRead(fileName)
    {
        array = new byte[fs.Length];
        using (BinaryReader binReader = new BinaryReader(fs))
        {
            array = binReader.ReadBytes((int)fs.Length);
        }

        // Create an OracleParameter to transmit the Blob
        param.OracleDbType = OracleDbType.Blob;
        param.ParameterName = "BlobParameter";
        param.Value = array;  // <-- file bytes are here
    }
    fileName = fileName.Split('\\')[fileName.Split('\\').Length-1]; // gets last segment of the whole path to just get the name

    string fileType = fileName.Split('.')[1];
    if (fileType == "doc" || fileType == "docx" || fileType == "pdf")
        fileType = "application\\" + fileType;
    else
        fileType = "image\\" + fileType;

    // SQL string containing reference to BlobParameter named above
    string sql = String.Format("INSERT INTO YOUR_TABLE (FILE_NAME, FILE_TYPE, FILE_SIZE, FILE_CONTENTS, LAST_MODIFIED) VALUES ('{0}','{1}',{2},:BlobParamerter, SYSDATE)", fileName, fileType, array.Length);

    // Do Oracle Update
    RunCommand(sql, param);
}

在使用ADO执行Oracle更新操作时:

public void RunCommand(string strSQL, OracleParameter param)
{
    OracleConnection oraConn = null;
    OracleCommand oraCmd = null;
    try
    {
        string connString = GetConnString();
        oraConn = OracleConnection(connString);
        using (oraConn)
        {
            if (OraConnection.State == ConnectionState.Open)
                OraConnection.Close();

            OraConnection.Open();

            oraCmd = new OracleCommand(strSQL, oraConnection);

            // Add your OracleParameter
            if (param != null)
                OraCommand.Parameters.Add(param);

            // Execute the command
            OraCommand.ExecuteNonQuery();
        }
    }
    catch (OracleException err)
    {
       // handle exception 
    }
    finally
    {
       OraConnction.Close();
    }
}

private string GetConnString()
{
    string host = System.Configuration.ConfigurationManager.AppSettings["host"].ToString();
    string port = System.Configuration.ConfigurationManager.AppSettings["port"].ToString();
    string serviceName = System.Configuration.ConfigurationManager.AppSettings["svcName"].ToString();
    string schemaName = System.Configuration.ConfigurationManager.AppSettings["schemaName"].ToString();
    string pword = System.Configuration.ConfigurationManager.AppSettings["pword"].ToString(); // hopefully encrypted

    if (String.IsNullOrEmpty(host) || String.IsNullOrEmpty(port) || String.IsNullOrEmpty(serviceName) || String.IsNullOrEmpty(schemaName) || String.IsNullOrEmpty(pword))
    {
        return "Missing Param";
    }
    else
    {
        pword = decodePassword(pword);  // decrypt here
        return String.Format(
           "Data Source=(DESCRIPTION =(ADDRESS = ( PROTOCOL = TCP)(HOST = {2})(PORT = {3}))(CONNECT_DATA =(SID = {4})));User Id={0};Password={1};",
           user,
           pword,
           host,
           port,
           serviceName
           );
    }
}

FILE_CONTENTS列的数据类型为 BLOBFILE_SIZE 的数据类型为 NUMBER(10, 0)LAST_MODIFIEDDATE 类型,其余数据类型是NVARCHAR2(250)


3
你正在使用哪个数据库?通常你不会把文件保存到数据库中,但我认为SQL 2008支持这样做...
文件是二进制数据,因此UTF-8在这里并不重要。
当你尝试将字符串转换为字节数组时,UTF-8很重要...而不是将文件转换为字节数组。

2

确认我能够在MS SQL Server 2012和2014上使用MadBoy发布的答案并由Otiel编辑,除了先前列出的版本之外,使用varbinary(MAX)列。

如果您想知道为什么无法在SQL Server表设计器中使用"Filestream"(在另一个答案中注明)作为数据类型,或者为什么无法使用T-SQL设置列的数据类型为"Filestream",那是因为FILESTREAM是varbinary(MAX)数据类型的存储属性。它本身不是一种数据类型。

请参阅以下文章以设置和启用数据库上的FILESTREAM: https://msdn.microsoft.com/en-us/library/cc645923(v=sql.120).aspx

http://www.kodyaz.com/t-sql/default-filestream-filegroup-is-not-available-in-database.aspx

配置完成后,可以添加启用filestream的varbinary(max)列,如下所示:

ALTER TABLE TableName

ADD ColumnName varbinary(max) FILESTREAM NULL

GO


1

请看这个,你可能会更容易地找到你问题的答案

使用:

using System.IO;
using System.Data.SqlClient;

代码:

private void Form1_Load(object sender, EventArgs e)
{
    display();
}
byte[] filebyte = null; 
SqlConnection sqlcon = new SqlConnection("Data Source=.;Initial Catalog=test programin;Integrated Security=True");
SqlCommand sqlcmnd = new SqlCommand();
void display ()
{
    DataSet dtset = new DataSet();
    SqlDataAdapter sqldta = new SqlDataAdapter("select name from tbl_down_up",sqlcon);
    sqldta.Fill(dtset, "tbl_down_up");
    dataGridView1.DataSource = dtset;
    dataGridView1.DataMember = "tbl_down_up";
    dataGridView1.Columns[0].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
}
private void btnup_Click(object sender, EventArgs e)
{
    OpenFileDialog ofd = new OpenFileDialog();
    ofd.Filter = "all file|*.*";
    if(ofd.ShowDialog()==DialogResult.OK)
    {
        FileStream fs = new FileStream(ofd.FileName, FileMode.Open);
        MemoryStream ms = new MemoryStream();
        fs.CopyTo(ms);
        filebyte = ms.ToArray();
        string[] filename = ofd.FileName.Split('\\');
        sqlcmnd = new SqlCommand("insert into tbl_down_up(name,data)values(@name,@data)",sqlcon);
        sqlcmnd.Parameters.AddWithValue("@name",filename[filename.Length-1]);
        sqlcmnd.Parameters.AddWithValue("@data",SqlDbType.VarBinary).Value=filebyte;
        sqlcon.Open();
        sqlcmnd.ExecuteNonQuery();
        sqlcon.Close();
        sqlcmnd.Parameters.Clear();
        display();
    }
}

private void btndown_Click(object sender, EventArgs e)
{
    SaveFileDialog sfd = new SaveFileDialog();
    string[] filename = dataGridView1[0, dataGridView1.CurrentRow.Index].Value.ToString().Split('.');
    sfd.Filter = "type file " + filename[filename.Length - 1] + " |*." + filename[filename.Length - 1];
    sfd.FileName = dataGridView1[0, dataGridView1.CurrentRow.Index].Value.ToString();
    if (sfd.ShowDialog() == DialogResult.OK)
    {
        FileStream fs = new FileStream(sfd.FileName, FileMode.Create);
        sqlcmnd = new SqlCommand("select data from tbl_down_up where name ='"+dataGridView1[0,dataGridView1.CurrentRow.Index].Value.ToString()+"'", sqlcon);sqlcon.Open();
        SqlDataReader dr = sqlcmnd.ExecuteReader();
        while (dr.Read())
        {
            filebyte = (byte[])dr[0];
        }
        sqlcon.Close();
        fs.Write(filebyte, 0, filebyte.Length);
        fs.Close();
        display();
    }
}

private void btndel_Click(object sender, EventArgs e)
{
    sqlcmnd = new SqlCommand("delete from tbl_down_up where name =N'" + dataGridView1[0, dataGridView1.CurrentRow.Index].Value.ToString() + "'", sqlcon);
    sqlcon.Open();
    sqlcmnd.ExecuteNonQuery();
    sqlcon.Close();
    display();
}

表单相关的视频: form1

SQL Server中tbl_down_up表的图片: tbl_down_up


1

是的,一般来说,在数据库中存储文件的最佳方式是将字节数组保存在一个BLOB列中。您可能还需要一些其他列来存储文件的元数据,例如名称、扩展名等。

将文件存储在数据库中并不总是一个好主意 - 例如,如果在数据库中存储文件,数据库的大小将会迅速增长。但这完全取决于您的使用场景。


“Blob”适用于Oracle,而不适用于SQL Server。 - vapcguy
@vapcguy 这个问题(以及这个答案)没有指定特定的数据库。 - Steve Smith
@SteveSmith 不是真的。他们询问了一个“var binary”字段。这些在Oracle中不存在,但在SQL Server中存在。 - vapcguy
1
假设 OP 特别指的是 "varbinary",这些也存在于 Maria/MySQL 中。然而,我认为可以安全地假设 OP 是在询问在任何品牌的数据库中存储二进制数据的实际情况和一般优缺点。 - Steve Smith
1
@SteveSmith 很有趣。不知道那个,谢谢。 - vapcguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接