分层应用:将文件存储在数据库的文件流中

发布于 2024-12-28 08:22:03 字数 2979 浏览 2 评论 0原文

对于 asp.Net MVC 项目,我需要处理大文件(主要是 200-300Mo,有时 1Go)。

我将它们存储在数据库中(出于备份原因/一致性原因)。

我担心性能问题,所以我想尽可能避免在程序中的任何位置使用字节数组,目标是在每个地方使用流。

我有一个分层应用程序,这主要意味着我有几个“DataStore”,它们负责连接并从数据库检索/插入/更新数据。

由于 EF 目前不支持 Filestream,因此我通过简单的 Sql 请求处理“文件部分”。我在这里阅读了一篇关于文件流使用的好文章: http://blog.tallan.com/2011/08/22/using-sqlfilestream-with-c-to-access-sql-server-filestream-data/

和我还有一些其他问题,希望您能帮助我/为我指出好的方向:

  • 由于我有一个分层应用程序,一旦我实例化了 SQLFileStream 对象,我可以处理 SqlCommand/Sql 连接/事务范围吗?
  • 如果没有,我该如何关闭它们?
  • 在前面的链接中,有一个示例展示了如何在 ASP 中使用它。但由于我使用的是 ASP.Net MVC,是否有一个可以直接将文件流式传输到浏览器的助手?因为我发现了很多将二进制数据返回到浏览器的示例,但就目前而言,我发现的所有示例基本上都使用 Stream.ToArray() 之类的东西来填充字节数组并将其返回到浏览器。我发现我可以返回一个 FileStreamResult ,它可以接受参数 Stream 。这是正确的方向吗?

(我目前不关心上传大文件,因为它们是由重型客户端插入数据库中的)

编辑

(对肮脏的代码感到抱歉,只是这里没有 50 种不同的方法。 我又进行了几次尝试,但由于分离的部分(我们生成层的位置和使用它的位置),我目前陷入了“读取”部分:

        SqlConnection conn = GetConnection();
        conn.Open();
        SqlCommand cmd = new SqlCommand(_selectMetaDataRequest, conn);
        cmd.Parameters.Add(_idFile, SqlDbType.Int).Value = idFile;
        SqlDataReader rdr = cmd.ExecuteReader();
        rdr.Read();
        string serverPath = rdr.GetSqlString(0).Value;
        byte[] serverTxn = rdr.GetSqlBinary(1).Value;
        rdr.Close();
        return new SqlFileStream(serverPath, serverTxn, FileAccess.Read);

但我在 rdr 处遇到异常。 GetSqlBinary(1).Value 因为 GET_FILESTREAM_TRANSACTION_CONTEXT 返回 null。我在此处发现这是由于缺少事务造成的。

我尝试使用“TransactionScope”+其 .Complete(); 调用。不会改变任何东西。

我尝试执行 BEGIN TRANSACTION,如上一个链接所示:

 SqlConnection 连接 = GetConnection();
        连接.Open();
        SqlCommand cmd = new SqlCommand();
        cmd.CommandText = "BEGIN TRANSACTION";
        cmd.CommandType = CommandType.Text;
        cmd.Connection = connection;
        cmd.ExecuteNonQuery();

        cmd = new SqlCommand(_selectMetaDataRequest, connection);
        cmd.Parameters.Add(_idFile, SqlDbType.Int).Value = idFile;
        SqlDataReader rdr = cmd.ExecuteReader();
        rdr.Read();
        string serverPath = rdr.GetSqlString(0).Value;
        byte[] serverTxn = rdr.GetSqlBinary(1).Value;
        rdr.Close();
        SqlFileStream sqlFileStream = new SqlFileStream(serverPath, serverTxn, FileAccess.Read);
 cmd = new SqlCommand();
        cmd.CommandText = "提交事务";
        cmd.CommandType = CommandType.Text;
        cmd.Connection = 连接;
        cmd.ExecuteNonQuery();

但它在第一个“ExecuteNonQuery”上崩溃,并出现异常“在 MARS 批处理中启动的事务在批处理结束时仍然处于活动状态。事务已回滚。” 但这是第一个查询已执行!

For an asp.Net MVC project, I will need to handle large files( mostly 200-300Mo, sometime 1Go).

I will store them in the database(for backup reasons/consistency reason).

I'm concerned by performance issue, so I want to avoid all I can to have a array of byte anywhere in the program, the goal is then to work with stream every where.

I've a layered application, which mostly means that I've several "DataStore", which are responsible to connect and retrieve/insert/update data from the database.

Since EF doesn't support Filestream for now, I'm handling the "File part" through simple Sql requests. I've read a good article on filestream usage here: http://blog.tallan.com/2011/08/22/using-sqlfilestream-with-c-to-access-sql-server-filestream-data/

And I've some additional questions, which I hope you can help me/point me to the good direction:

  • Since I've a layered application, once I've my SQLFileStream object instantiated, could I dispose SqlCommand/Sql Connection/Transaction scope?
  • If not, how I'm supposed to close them?
  • In the previous link, there is an example which show how to use it with ASP. But since I'm using ASP.Net MVC, isn't there an helper which is directly able to stream a file to the browser? Because I found many example of return binary data to browser, but for now, all example I found make basically something like Stream.ToArray() to fill an array of byte and return it to the browser. I found that I can return a FileStreamResult which can take in parameter a Stream. Is that the right direction?

(I'm not currently concerned by uploading big files, since they are inserted by an heavy client in the database)

EDIT

(Sorry for the dirty code, it's only to not have 50 different methods here.
I've made a few more try, and I'm currently stuck with the "read" part, because of separated part(where we generate the layer and where we consume it):

        SqlConnection conn = GetConnection();
        conn.Open();
        SqlCommand cmd = new SqlCommand(_selectMetaDataRequest, conn);
        cmd.Parameters.Add(_idFile, SqlDbType.Int).Value = idFile;
        SqlDataReader rdr = cmd.ExecuteReader();
        rdr.Read();
        string serverPath = rdr.GetSqlString(0).Value;
        byte[] serverTxn = rdr.GetSqlBinary(1).Value;
        rdr.Close();
        return new SqlFileStream(serverPath, serverTxn, FileAccess.Read);

But I get an exception at rdr.GetSqlBinary(1).Value because GET_FILESTREAM_TRANSACTION_CONTEXT return null. I found here that this is due the missing transaction.

I tried with a "TransactionScope"+its .Complete(); call. Doesn't change anything.

I tried to do a BEGIN TRANSACTION like showed in the previous link:

        SqlConnection connection = GetConnection();
        connection.Open();
        SqlCommand cmd = new SqlCommand();
        cmd.CommandText = "BEGIN TRANSACTION";
        cmd.CommandType = CommandType.Text;
        cmd.Connection = connection;
        cmd.ExecuteNonQuery();

        cmd = new SqlCommand(_selectMetaDataRequest, connection);
        cmd.Parameters.Add(_idFile, SqlDbType.Int).Value = idFile;
        SqlDataReader rdr = cmd.ExecuteReader();
        rdr.Read();
        string serverPath = rdr.GetSqlString(0).Value;
        byte[] serverTxn = rdr.GetSqlBinary(1).Value;
        rdr.Close();
        SqlFileStream sqlFileStream = new SqlFileStream(serverPath, serverTxn, FileAccess.Read);
      cmd = new SqlCommand();
        cmd.CommandText = "COMMIT TRANSACTION";
        cmd.CommandType = CommandType.Text;
        cmd.Connection = connection;
        cmd.ExecuteNonQuery();

But it crashes on the first "ExecuteNonQuery" with the exception "A transaction that was started in a MARS batch is still active at the end of the batch. The transaction is rolled back." But it's the FIRST query executed!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

女皇必胜 2025-01-04 08:22:03

让我们举个例子。我们可以首先定义一个合约来描述我们愿意执行的操作:

public interface IPhotosRepository
{
    void GetPhoto(int photoId, Stream output);
}

我们稍后会看到实现。

现在我们可以定义一个自定义操作结果:

public class PhotoResult : FileResult
{
    private readonly Action<int, Stream> _fetchPhoto;
    private readonly int _photoId;
    public PhotoResult(int photoId, Action<int, Stream> fetchPhoto, string contentType): base(contentType)
    {
        _photoId = photoId;
        _fetchPhoto = fetchPhoto;
    }

    protected override void WriteFile(HttpResponseBase response)
    {
        _fetchPhoto(_photoId, response.OutputStream);
    }
}

然后是一个允许我们显示照片的控制器:

public class HomeController : Controller
{
    private readonly IPhotosRepository _repository;

    public HomeController(IPhotosRepository repository)
    {
        _repository = repository;
    }

    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Photo(int photoId)
    {
        return new PhotoResult(photoId, _repository.GetPhoto, "image/jpg");
    }
}

以及一个相应的视图,我们将在其中使用 标记显示照片Photo 操作:

<img src="@Url.Action("photo", new { photoid = 123 })" alt="" />

现在最后一部分当然是存储库的实现,它可能看起来类似于:

public class PhotosRepositorySql : IPhotosRepository
{
    private readonly string _connectionString;
    public PhotosRepositorySql(string connectionString)
    {
        _connectionString = connectionString;
    }

    public void GetPhoto(int photoId, Stream output)
    {
        using (var ts = new TransactionScope())
        using (var conn = new SqlConnection(_connectionString))
        using (var cmd = conn.CreateCommand())
        {
            conn.Open();
            cmd.CommandText =
            @"
                SELECT 
                    Photo.PathName() as path, 
                    GET_FILESTREAM_TRANSACTION_CONTEXT() as txnToken 
                FROM 
                    PhotoAlbum 
                WHERE 
                    PhotoId = @PhotoId
            ";
            cmd.Parameters.AddWithValue("@PhotoId", photoId);
            using (var reader = cmd.ExecuteReader())
            {
                if (reader.Read())
                {
                    var path = reader.GetString(reader.GetOrdinal("path"));
                    var txnToken = reader.GetSqlBinary(reader.GetOrdinal("txnToken")).Value;
                    using (var stream = new SqlFileStream(path, txnToken, FileAccess.Read))
                    {
                        stream.CopyTo(output);
                    }
                }
            }
            ts.Complete();
        }
    }    
}

现在剩下的就是指示您最喜欢的 DI 框架使用 PhotosRepositorySql.

这种技术允许您有效地处理任意大文件,因为它永远不会将整个流加载到内存中。

Let's have an example. We could start by defining a contract that will describe the operation we are willing to perform:

public interface IPhotosRepository
{
    void GetPhoto(int photoId, Stream output);
}

We will see the implementation later.

Now we could define a custom action result:

public class PhotoResult : FileResult
{
    private readonly Action<int, Stream> _fetchPhoto;
    private readonly int _photoId;
    public PhotoResult(int photoId, Action<int, Stream> fetchPhoto, string contentType): base(contentType)
    {
        _photoId = photoId;
        _fetchPhoto = fetchPhoto;
    }

    protected override void WriteFile(HttpResponseBase response)
    {
        _fetchPhoto(_photoId, response.OutputStream);
    }
}

then a controller which will allow us to show the photo:

public class HomeController : Controller
{
    private readonly IPhotosRepository _repository;

    public HomeController(IPhotosRepository repository)
    {
        _repository = repository;
    }

    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Photo(int photoId)
    {
        return new PhotoResult(photoId, _repository.GetPhoto, "image/jpg");
    }
}

and a corresponding view in which we are going to show the photo in an <img> tag using the Photo action:

<img src="@Url.Action("photo", new { photoid = 123 })" alt="" />

Now the last part is of course the implementation of the repository which might look something along the lines of:

public class PhotosRepositorySql : IPhotosRepository
{
    private readonly string _connectionString;
    public PhotosRepositorySql(string connectionString)
    {
        _connectionString = connectionString;
    }

    public void GetPhoto(int photoId, Stream output)
    {
        using (var ts = new TransactionScope())
        using (var conn = new SqlConnection(_connectionString))
        using (var cmd = conn.CreateCommand())
        {
            conn.Open();
            cmd.CommandText =
            @"
                SELECT 
                    Photo.PathName() as path, 
                    GET_FILESTREAM_TRANSACTION_CONTEXT() as txnToken 
                FROM 
                    PhotoAlbum 
                WHERE 
                    PhotoId = @PhotoId
            ";
            cmd.Parameters.AddWithValue("@PhotoId", photoId);
            using (var reader = cmd.ExecuteReader())
            {
                if (reader.Read())
                {
                    var path = reader.GetString(reader.GetOrdinal("path"));
                    var txnToken = reader.GetSqlBinary(reader.GetOrdinal("txnToken")).Value;
                    using (var stream = new SqlFileStream(path, txnToken, FileAccess.Read))
                    {
                        stream.CopyTo(output);
                    }
                }
            }
            ts.Complete();
        }
    }    
}

All that's left now is to instruct your favorite DI framework to use PhotosRepositorySql.

This technique allows you to efficiently work with arbitrary big files as it never loads the entire stream into memory.

尘曦 2025-01-04 08:22:03

检查此答案以获取成功下载最大 4GB 文件的示例:https://stackoverflow.com/a/3363015/234415

使用 Filestream 时需要记住的几个有趣的点:

  • 文件不计入大小限制的一部分(因此它可以很好地与SQLEXPRESS 对于 SQL 2008 R2 版本有 10GB 的限制,因此您可以免费拥有 10GB 的数据和 TB 的文件,这很酷
  • 。使用集成安全性 (MSDN),它不支持 SQL 登录,如果您想使用共享主机,这很烦人!

Check this answer for an example of successfully downloading files of up to 4GB: https://stackoverflow.com/a/3363015/234415

A couple of interesting points to remember about using Filestream:

  • The files do not count as part of the size limit (so it works nicely with SQLEXPRESS which has a 10GB limit for the SQL 2008 R2 version. So you can have 10GB of data and terabytes of files, all for free, which is cool.
  • You HAVE to use integrated security (MSDN) , it doesn't support SQL login. This is annoying if you want to use shared hosting!
长安忆 2025-01-04 08:22:03

SQL 数据库在处理大文件时效果非常糟糕,并且也有注册表大小限制。
我建议使用 NoSQL 数据库(如 MongoDB)来存储巨大的文件。您可以使用两个数据库(SQL 用于纯数据,NoSQL 用于文件)没有问题。

我知道这不是您想要的答案,但却是获得良好表现的最佳方式。

A SQL Database works very bad with huge files, and there are registry size limit too.
I recommend to use a NoSQL databse (like MongoDB) to store that huge files. you can use two databases, (SQL for plain data, NoSQL for files) there no problem.

I know this is not the answer you want, but is the best way to have good performance.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文