
ASP.NET Core断点续传 在ASP.NET WebAPi写过完整的断点续传文章,目前我对ASP.NET Core仅止于整体上会用,对于原理还未去深入学习,由于有园友想看断点续传在ASP.NET Core中的具体实现,于是借助在家中休息时间看了下ASP.NET Core是否支持断点续传以及支持后具体实现以及相关APi,花了一点时间,本文而由此而生。 断点续传基础 此前在ASP.NET WebAPi中对于一些基础内容已经详细讲解过,同时也进行了封装,所以再处理ASP.NET Core不过是APi使用不同罢了,断点续传重点在于AcceptRange和ContentRange以及对应响应请求头设置,其余和ASP.NET WebAPi使用别无二致。 在ASP.NET WebAPi中我们封装了对文件的操作接口IFileProvider和具体实现FileProvider,在控制器中我们是直接实例化,在ASP.NET Core中有了依赖注入,我们可直接借助控制器构造函数注入接口。 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddScoped<IFileProvider, FileProvider>(); } 当然前提是新建一个ASP.NET Core Web应用程序,然后我们新建一个DownLoadController控制器。在ASP.NET WebAPi中对于请求-响应机制对象是HttpRequestMessage和HttpResponseMessage,而在ASP.NET Core则是HttpRequest和HttpResponse对象。那么我们在控制器中如何获取这两个对象中呢?如果我们稍微有点经验的话就能明了请求和响应对象必然存储在上下文中,那么我们又如何获取上下文呢?通过IHttpContextAccessor接口获取。所以在控制器构造函数中获取文件接口和上下文以及对应的常量如下: private const int BufferSize = 80 * 1024; private const string MimeType = "application/octet-stream"; public IFileProvider _fileProvider { get; set; } private IHttpContextAccessor _contextAccessor; private HttpContext _context { get { return _contextAccessor.HttpContext; } } public FileDownloadController( IFileProvider fileProvider, IHttpContextAccessor contextAccessor) { _fileProvider = fileProvider; _contextAccessor = contextAccessor; } 在ASP.NET WebAPi中获取请求头Range利用请求中的Headers属性获取,但在ASP.NET Core中则需要通过请求中的GetTypedHeaders()方法获取。 ASP.NET Core对于请求头中参数的获取和值的设置更加友好,比如我们要获取请求头中的请求类型,在ASP.NET WebAPi中我们指定字符串Request.Headers["Content-Type"],而在ASP.NET Core中则对应的是Request.Headers[HeaderNames.ContentType],直接通过枚举指定,如此一来则省事多了。最终在DownLoadController控制器中对于在ASP.NET Core中断点续传的整个逻辑如下: public class FileDownloadController { private const int BufferSize = 80 * 1024; private const string MimeType = "application/octet-stream"; public IFileProvider _fileProvider { get; set; } private IHttpContextAccessor _contextAccessor; private HttpContext _context { get { return _contextAccessor.HttpContext; } } public FileDownloadController( IFileProvider fileProvider, IHttpContextAccessor contextAccessor) { _fileProvider = fileProvider; _contextAccessor = contextAccessor; } /// <summary> /// 下载文件 /// </summary> /// <param name="fileName"></param> /// <returns></returns> [HttpGet("api/download")] public IActionResult GetFile(string fileName) { fileName = "cn_windows_8_1_x64_dvd_2707237.iso"; if (!_fileProvider.Exists(fileName)) { return new StatusCodeResult(StatusCodes.Status404NotFound); } //获取下载文件长度 var fileLength = _fileProvider.GetLength(fileName); //初始化下载文件信息 var fileInfo = GetFileInfoFromRequest(_context.Request, fileLength); //获取剩余部分文件流 var stream = new PartialContentFileStream(_fileProvider.Open(fileName), fileInfo.From, fileInfo.To); //设置响应 请求头 SetResponseHeaders(_context.Response, fileInfo, fileLength, fileName); return new FileStreamResult(stream, new MediaTypeHeaderValue(MimeType)); } /// <summary> /// 根据请求信息赋予封装的文件信息类 /// </summary> /// <param name="request"></param> /// <param name="entityLength"></param> /// <returns></returns> private FileInfo GetFileInfoFromRequest(HttpRequest request, long entityLength) { var fileInfo = new FileInfo { From = 0, To = entityLength - 1, IsPartial = false, Length = entityLength }; var requestHeaders = request.GetTypedHeaders(); if (requestHeaders.Range != null && requestHeaders.Range.Ranges.Count > 0) { var range = requestHeaders.Range.Ranges.FirstOrDefault(); if (range.From.HasValue && range.From < 0 || range.To.HasValue && range.To > entityLength - 1) { return null; } var start = range.From; var end = range.To; if (start.HasValue) { if (start.Value >= entityLength) { return null; } if (!end.HasValue || end.Value >= entityLength) { end = entityLength - 1; } } else { if (end.Value == 0) { return null; } var bytes = Math.Min(end.Value, entityLength); start = entityLength - bytes; end = start + bytes - 1; } fileInfo.IsPartial = true; fileInfo.Length = end.Value - start.Value + 1; } return fileInfo; } /// <summary> /// 设置响应头信息 /// </summary> /// <param name="response"></param> /// <param name="fileInfo"></param> /// <param name="fileLength"></param> /// <param name="fileName"></param> private void SetResponseHeaders(HttpResponse response, FileInfo fileInfo, long fileLength, string fileName) { response.Headers[HeaderNames.AcceptRanges] = "bytes"; response.StatusCode = fileInfo.IsPartial ? StatusCodes.Status206PartialContent : StatusCodes.Status200OK; var contentDisposition = new ContentDispositionHeaderValue("attachment"); contentDisposition.SetHttpFileName(fileName); response.Headers[HeaderNames.ContentDisposition] = contentDisposition.ToString(); response.Headers[HeaderNames.ContentType] = MimeType; response.Headers[HeaderNames.ContentLength] = fileInfo.Length.ToString(); if (fileInfo.IsPartial) { response.Headers[HeaderNames.ContentRange] = new ContentRangeHeaderValue(fileInfo.From, fileInfo.To, fileLength).ToString(); } } } ASP.NET Core中FileResult、FileStreamResult、FilePhsicalResult都已支持断点续传,如果对于很小的文件直接下载即可,如果稍微大一点文件则可利用断点续传即可,如果对于非常大的文件则需要自定义流来下载这样更高效,比如对于获取视频流文件。上述我们依然是采取自定义流的形式来实现断点续传,若对其中封装的自定义流和接口有疑惑请移步右上角我的github参看ASP.NET WebAPi具体实现。无论是ASP.NET WebAPi和ASP.NET Core断点续传都实现了核心逻辑,对于一些细节未考虑其中,希望对想学习断点续传的您有所帮助,祝您阅读愉快,新年快乐!
上一篇我们探讨了在静态语句中使用WHERE Column=@Param OR @Param IS NULL的问题,有对OPTION(COMPILE)的评论,那这节我们来探讨OPTION(COMPILE)的问题。 探讨OPTION(COMPILE)问题 在SQL SERVER中任何时候第一次调用存储过程时,此时存储过程将会被SQL SERVER优化且查询计划在内存中会被缓存。由于查询计划缓存,当运行相同的存储过程时,它都将使用相同的查询计划,从而无需每次运行时对同一存储过程进行优化和编译。因此,如果我们需要每天运行相同的存储过程若干次,那么可以节省大量的时间和硬件资源。 如果每次运行的存储过程中的在WHERE子句中具有相同的参数,则重复使用存储过程的相同查询计划是有意义的。但是,如果运行相同的存储过程,但是参数的值会改变呢?发生什么取决于参数的典型性。如果存储过程的参数的值从执行到执行相似,那么缓存的查询计划将正常工作,查询将按照执行最佳来。但是,如果参数不是典型的,那么被重用的缓存查询计划可能不是最优的,导致查询运行更慢,因为它使用的查询计划并不是真正为所使用的参数设计的。下面我们借助AdventureWorks2012实例数据库来用实例讲解上述所描述的情况。 DECLARE @AddressLine1 NVARCHAR(60) = NULL, @AddressLine2 NVARCHAR(60) = NULL, @City NVARCHAR(30) = NULL, @PostalCode NVARCHAR(15) = NULL, @StateProvinceID INT = NULL SET @City = 'Bothell' SET @PostalCode = '98011' SET @StateProvinceID = 79 DECLARE @SQL NVARCHAR(MAX),@ColumnName VARCHAR(4000),@ParamDefinition NVARCHAR(500) SET @ColumnName = 'a.AddressID, a.AddressLine1, a.AddressLine2, a.City, a.StateProvinceID, a.PostalCode, a.rowguid' SET @SQL = 'SELECT ' + @ColumnName + ' FROM Person.Address AS a WHERE 1 = 1' IF (@AddressLine1 IS NOT NULL) SET @SQL = @SQL + ' AND a.AddressLine1 LIKE ''%'' + @AddressLine1 + ''%''' IF (@AddressLine2 IS NOT NULL) SET @SQL = @SQL + ' AND a.AddressLine2 LIKE ''%'' + @AddressLine2 + ''%''' IF (@City IS NOT NULL) SET @SQL = @SQL + ' AND a.City LIKE ''%'' + @City + ''%''' IF (@PostalCode IS NOT NULL) SET @SQL = @SQL + ' AND a.PostalCode LIKE ''%'' + @PostalCode + ''%''' IF (@StateProvinceID IS NOT NULL) SET @SQL = @SQL + ' AND a.StateProvinceID = @StateProvinceID' SET @ParamDefinition = N'@AddressLine1 NVARCHAR(60), @AddressLine2 NVARCHAR(60), @City NVARCHAR(30), @PostalCode NVARCHAR(15), @StateProvinceID INT' EXECUTE sp_executesql @SQL,@ParamDefinition, @AddressLine1 = @AddressLine1, @AddressLine2 = @AddressLine2, @City = @City, @PostalCode = @PostalCode, @StateProvinceID = @StateProvinceID GO 我们运行上述查询1次,看到查询结果如下和计划缓存次数如下: 此时我们将外部变量StateProvinceID类型修改为SMALLINT,然后再来运行查询和缓存计划,此时会出现查询计划使用次数是为2,还是出现两条次数都为1呢? 此时我们再来将动态SQL中内部变量StateProvinceID类型修改为SMALLINT,此时会出现查询计划使用次数是为3,还是出现两条,次数分别为2和1呢? 由上可知,如果我们修改外部变量参数类型不会影响查询计划缓存即会达到重用目的,若修改动态SQL内部变量参数类型则不会重用查询计划缓存。 大多数情况下,我们可能不需要担心上述问题。但是,在某些情况下,假设从查询的执行到执行的参数变化很大,则会引起问题。 如果我们确定存储过程通常运行正常,但有时运行缓慢,则很可能会看到上述问题。在这种情况下,我们可以做的是改变存储过程,并添加WITH RECOMPILE选项。 添加此选项后,存储过程将始终重新编译自身,并在每次运行时创建一个新的查询计划。当然这会消除查询计划重用的好处,但确保了每次运行查询时都使用正确的查询计划。如果存储过程中有多个查询,那么它将重新编译存储过程中的所有查询,即使那些不受非典型参数影响的查询也是如此。 讲完OPTION(COMPILE),接下来我们讲讲如何创建高性能的存储过程。有些童鞋可能会创建如下存储过程。 CREATE PROC [dbo].[HighPerformanceExample] ( @AddressLine1 NVARCHAR(60) = NULL, @AddressLine2 NVARCHAR(60) = NULL, @City NVARCHAR(30) = NULL, @PostalCode NVARCHAR(15) = NULL, @StateProvinceID SMALLINT = NULL ) AS SET NOCOUNT ON SELECT a.AddressID, a.AddressLine1, a.AddressLine2, a.City, a.StateProvinceID, a.PostalCode, a.rowguid FROM Person.Address AS a WHERE (a.AddressLine1 = @AddressLine1 OR @AddressLine1 IS NULL) AND (a.AddressLine2 = @AddressLine2 OR @AddressLine2 IS NULL) AND (a.City = @City OR @City IS NULL) AND (a.PostalCode = @PostalCode OR @PostalCode IS NULL) AND (a.StateProvinceID = @StateProvinceID OR @StateProvinceID IS NULL) --或者 SELECT a.AddressID, a.AddressLine1, a.AddressLine2, a.City, a.StateProvinceID, a.PostalCode, a.rowguid FROM Person.Address AS a WHERE a.AddressLine1 = COALESCE(@AddressLine1, a.AddressLine1) AND a.AddressLine2 = COALESCE(@AddressLine2, a.AddressLine2) AND a.City = COALESCE(@City, a.City) AND a.PostalCode = COALESCE(@PostalCode, a.PostalCode) AND a.StateProvinceID = COALESCE(@StateProvinceID, a.StateProvinceID) --或者 SELECT a.AddressID, a.AddressLine1, a.AddressLine2, a.City, a.StateProvinceID, a.PostalCode, a.rowguid FROM Person.Address AS a WHERE a.AddressLine1 = CASE WHEN @AddressLine1 IS NULL THEN a.AddressLine1 ELSE @AddressLine1 END AND a.AddressLine2 = CASE WHEN @AddressLine2 IS NULL THEN a.AddressLine1 ELSE @AddressLine2 END AND a.City = CASE WHEN @City IS NULL THEN a.City ELSE @City END AND a.PostalCode = CASE WHEN @PostalCode IS NULL THEN a.PostalCode ELSE @PostalCode END AND a.StateProvinceID = CASE WHEN @StateProvinceID IS NULL THEN a.StateProvinceID ELSE @StateProvinceID END GO SET NOCOUNT OFF 上述无论怎样执行都将表现的非常糟糕。因为SQL SERVER不能将其很好地进行优化,如果这是由不同的参数组合产生,那么我们可能会得到一个绝对糟糕的计划。不难理解,当执行一个存储过程,并且还没有生成一个查询缓存计划。所以,管理员可能会更新统计信息或强制重新编译(或者,甚至重新启动SQL Server)来尝试解决此问题,但这些都不是最佳解决方案。OPTION(COMPILE)重新编译是个好东西,但是我们是不是像如下简单加上重新编译就可以了呢。 SELECT ... FROM ... WHERE ... OPTION (RECOMPILE); 如果我们要使用重新编译,那么我们是否需要考虑以下两个问题呢? 如果我们知道一个特定的语句总是返回相同数量的行并使用相同的计划(并且我们已测试过并知道这一点),那么我们会正常创建存储过程并让计划得到缓存。 如果我们知道一个特定的语句从执行到执行是不一样的,最佳查询计划也会有所不同(我们也应该从执行多个测试样本中知道这一点),然后我们会如正常一样创建存储过程,然后使用OPTION(RECOMPILE)以确保语句的计划不会被存储过程缓存或保存。在每次执行时,存储过程将获得不同的参数,如此一来语句将在每次执行时得到一个新的计划。 上述已经描述的很明朗了,使用或者不使用重新编译的前提不过是需不需要查询计划缓存还是重新生成一个查询计划,但是我们怎么知道到底是否需要查询计划缓存呢?这就要看对创建的存储过程是否稳定了,如果稳定我们就从缓存中去取,否则使用重新编译查询。归根结底一句话概述:重新编译不稳定(可变)的计划,但为稳定(不可变)的计划,我们把它们放在缓存中重用。 为了实现这点,我们需要分析所查询的存储过程,例如在每个企业下有对应的用户,我们想象一下所呈现的UI界面,首先是所有用户,查询条件则是企业下拉框,然后是用户名或者员工工号等。当没有任何筛选条件时则走查询计划缓存,若选择企业,或者还选择了员工相关筛选条件则重新编译。类似如下存储过程。 CREATE PROC [dbo].[HighPerformanceExample] ( @AddressLine1 NVARCHAR(60) = NULL, @AddressLine2 NVARCHAR(60) = NULL, @City NVARCHAR(30) = NULL, @PostalCode NVARCHAR(15) = NULL, @StateProvinceID SMALLINT = NULL ) AS SET NOCOUNT ON DECLARE @SQL NVARCHAR(MAX),@ColumnName VARCHAR(4000),@ParamDefinition NVARCHAR(500),@Recompile BIT = 1; SET @ColumnName = 'a.AddressID, a.AddressLine1, a.AddressLine2, a.City, a.StateProvinceID, a.PostalCode, a.rowguid' SET @SQL = 'SELECT ' + @ColumnName + ' FROM Person.Address AS a WHERE 1 = 1' IF (@StateProvinceID IS NOT NULL) SET @SQL = @SQL + ' AND a.StateProvinceID = @StateProvinceID' IF (@AddressLine1 IS NOT NULL) SET @SQL = @SQL + ' AND a.AddressLine1 LIKE @AddressLine1' IF (@AddressLine2 IS NOT NULL) SET @SQL = @SQL + ' AND a.AddressLine2 LIKE @AddressLine2' IF (@City IS NOT NULL) SET @SQL = @SQL + ' AND a.City LIKE @City' IF (@PostalCode IS NOT NULL) SET @SQL = @SQL + ' AND a.PostalCode LIKE @PostalCode' IF (@StateProvinceID IS NOT NULL) SET @Recompile = 0 IF (PATINDEX('%[%_?]%',@AddressLine1) >= 4 OR PATINDEX('%[%_?]%', @AddressLine2) = 0) AND (PATINDEX('%[%_?]%', @City) >= 4 OR PATINDEX('%[%_?]%', @PostalCode) = 0) SET @Recompile = 0 IF @Recompile = 1 BEGIN SET @SQL = @SQL + N' OPTION(RECOMPILE)'; END; SET @ParamDefinition = N'@AddressLine1 NVARCHAR(60), @AddressLine2 NVARCHAR(60), @City NVARCHAR(30), @PostalCode NVARCHAR(15), @StateProvinceID SMALLINT' EXECUTE sp_executesql @SQL,@ParamDefinition, @AddressLine1 = @AddressLine1, @AddressLine2 = @AddressLine2, @City = @City, @PostalCode = @PostalCode, @StateProvinceID = @StateProvinceID GO SET NOCOUNT OFF 本节我们讲解了如何在存储过程中使用OPTION(COMPILE),并且使得存储过程性能达到最佳,我想这是根据实际场景分析存储过程相对来说首选和最佳的方法,以至于我们不必每次都重新编译。从而给我们长期更好的可扩展性。
之前我们已经讨论过动态SQL查询呢?这里为何再来探讨一番呢?因为其中还是存在一定问题,如标题所言,很多面试题也好或者有些博客也好都在说在执行动态SQL查询时sp_executesql的性能比exec好,但是事实真是如此?下面我们来一探究竟。 探讨sp_executesql和exec执行动态SQL查询性能 首先我们创建如下测试表。 CREATE TABLE dbo.TestDynamicSQL ( Col1 INT PRIMARY KEY , Col2 SMALLINT NOT NULL , CreatedTime DATETIME DEFAULT GETDATE() , OtherValue CHAR(10) DEFAULT 'Jeffcky' ) GO 接着再来插入数据,如下: INSERT dbo.TestDynamicSQL ( Col1, Col2 ) SELECT number + 1 , number FROM master..spt_values WHERE type = 'P' ORDER BY number 最终查询为如下测试数据: 接下来我们执行如下两个SQL查询语句,执行4次 SELECT * FROM dbo.TestDynamicSQL WHERE Col2 = 3 AND Col1 = 4 GO SELECT * FROM dbo.TestDynamicSQL WHERE Col2 = 4 AND Col1 = 5 GO 紧接着我们通过如下SQL语句来查询缓存计划。 SELECT q.text , cp.usecounts , cp.objtype , p.* , q.* , cp.plan_handle FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) p CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS q WHERE cp.cacheobjtype = 'Compiled Plan' AND q.text LIKE '%dbo.TestDynamicSQL%' AND q.text NOT LIKE '%sys.dm_exec_cached_plans %' 由上图可知,我们看到存在两个查询计划且每个执行了4次,也就是说每一次查询都会重新生成一个新的计划。清除查询计划缓存,通过如下命令: DBCC FREEPROCCACHE 我们继续往下走,我们接下来通过EXEC来执行动态SQL查询,如下,执行查询完毕后再来看看查询计划次数: DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 11 , @Col1 = 12 DECLARE @SQL VARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC (@SQL) GO DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 12 , @Col1 = 13 DECLARE @SQL VARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC (@SQL) GO 这个就不做过多解释,我们依然要清除查询计划缓存,我们再利用sp_executesql来查询,如下: DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 23 , @Col1 = 24 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC sp_executesql @SQL Go DECLARE @Col2 SMALLINT DECLARE @Col1 INT SELECT @Col2 = 22 , @Col1 = 23 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = ' + CONVERT(VARCHAR(10), @Col2) + ' and Col1 = ' + CONVERT(VARCHAR(10), @Col1) EXEC sp_executesql @SQL GO 对比exec执行动态SQL查询得到的结果是一模一样,正如我所演示的,我们有两个计划,每个执行次数为4。不是说sp_executesql执行动态SQL查询会重用计划缓存么,这是因为我们没有正确使用sp_executesql所以导致SQL引擎无法重用计划。 当参数值改变为语句是唯一变化时,可以使用sp_executesql代替存储过程多次执行Transact-SQL语句。 因为Transact-SQL语句本身保持不变,只有参数值发生变化,因此SQL Server查询优化器可能会重用为第一次执行生成的执行计划。 以下是正确参数化的查询方式,我们在字符串里面有一些变量,在执行的时候,我们通过其他变量传递值给它。 DECLARE @Col2 SMALLINT , @Col1 INT SELECT @Col2 = 3 , @Col1 = 4 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = @InnerCol2 and Col1 = @InnerCol1' DECLARE @ParmDefinition NVARCHAR(500) SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int' EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1 GO DECLARE @Col2 SMALLINT , @Col1 INT SELECT @Col2 = 3 , @Col1 = 4 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = @InnerCol2 and Col1 = @InnerCol1' DECLARE @ParmDefinition NVARCHAR(500) SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int' EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1 GO 我们看到只有一个计数为8的计划,而不是像我们上述那样运行查询。 我们也可以只需要声明一次,然后我们只需要在执行之前更改参数的值,如下: DECLARE @Col2 SMALLINT , @Col1 INT SELECT @Col2 = 3 , @Col1 = 4 DECLARE @SQL NVARCHAR(1000) SELECT @SQL = 'select * from dbo.TestDynamicSQL where Col2 = @InnerCol2 and Col1 = @InnerCol1' DECLARE @ParmDefinition NVARCHAR(500) SET @ParmDefinition = N'@InnerCol2 smallint ,@InnerCol1 int' EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1 --change param values and run the same query SELECT @Col2 = 2 , @Col1 = 3 EXEC sp_executesql @SQL, @ParmDefinition, @InnerCol2 = @Col2, @InnerCol1 = @Col1 最终查询计划缓存次数和上述正确方式一致。正确使用sp_executesql对于性能非常有利,而且使用sp_executesql还可以为我们提供一些EXEC无法实现的功能。比如如何得到一个表中的行数? 利用EXEC我们需要使用一个临时表和填充,而用sp_executesql我们只需要使用一个输出变量。 SET STATISTICS IO ON SET STATISTICS TIME ON --EXEC (SQL) DECLARE @Totalcount INT , @SQL NVARCHAR(100) CREATE TABLE #temp (Totalcount INT ) SELECT @SQL = 'Insert into #temp Select Count(*) from dbo.TestDynamicSQL' EXEC( @SQL) SELECT @Totalcount = Totalcount FROM #temp SELECT @Totalcount AS Totalcount DROP TABLE #temp GO --sp_executesql DECLARE @TableCount INT, @SQL NVARCHAR(100) SELECT @SQL = N'SELECT @InnerTableCount = COUNT(*) FROM dbo.TestDynamicSQL' EXEC SP_EXECUTESQL @SQL, N'@InnerTableCount INT OUTPUT', @TableCount OUTPUT SELECT @TableCount GO 当然除了EXEC无法实现的功能外,最重要的一点则是SP_EXECUTESQL能够防止SQL注入问题。 执行SQL动态查询SP_EXECUTESQL比EXEC性能更好,使得存储过程能够被重用,但是存储过程能够被重用的前提则是正确使用参数,使用参数化查询,否则SP_EXECUTESQL将不会提供任何性能益处。
前言 从.NET Core 1.0开始我们就将其应用到项目中,但是呢我对ASP.NET Core一些原理也还未开始研究,仅限于会用,不过园子中已有大量文章存在,借着有点空余时间,我们来讲讲如何利用ASP.NET Core结合Vue在IIS上运行。 ASP.NET Core结合Vue部署于IIS 关于安装Vue和Webpack则不再叙述,我们直接来创建ASP.NET Core应用程序或者通过dotnet new mvc创建ASP.NET Core应用程序 接下来在上述应用程序下通过如下命令创建Vue模板 vue init webpack my-project cd my-project npm install 接下来我们利用npm run dev启动Vue. 此时Vue也运行起来,我们则可以很嗨森的在本地进行测试了。完成了第一步,然后我们在生产环境实现前后分离,将上述ASP.NET Core应用程序部署到IIS作为一个单独站点,与此同时将Vue也作为一个单独站点。接下来我们将上述应用程序部署到IIS上。 在IIS上创建的站点aspnetcore,其基本设置中应用程序池当然为无托管代码,如果没有请自行下载.NET Core run time。 此时我们需要做的则是修改Vue配置文件,修改生成的Vue模板项目config文件夹下的Index.js文件来配置生成文件所在目录,如下: 接下来再运行npm run build命令将在wwwroot文件夹下生成vue文件。 然后我们创建前端站点将上述生成的文件放到该站点下,比如我创建的是VueDemo。 是不是一切看起来都是那么简单和easy呢,可能在你进行如上测试时会出现很多问题,我只是未做详细说明而已,比如没有权限访问啊,那就给定权限诺,默认情况下站点权限为IIS APPPOOL\DefaultAppPool ,要是权限不够就配置NET SERVICE或者EVERY ONE,再要么是创建的Vue站点无法访问出现对应的错误代码,此时需要通过Web平台安装路由重写程序。 总结 本节我们简短介绍了如何实现ASP.NET Core应用程序与前端框架(如Vue)前后分离,上述完成后接下来则是Vue进行接口调用了,有时间会更新利用Vue进行Ajax请求ASP.NET Core接口,如果上述创建过程中出现任何坑而无法解决可直接私信我,一一为你解答。see u。
前言 .NET Core项目利用EntityFramework Core作为数据访问层一直在进行中,一直没有过多的去关注背后生成的SQL语句,然后老大捞出日志文件一看,恩,有问题了,所以本文产生了,也是有点疑惑,若有知情者,还望告知。 EntityFramework Core忽略导航属性 在前面我们已经探讨过利用Serilog日志框架来输出日志,所以对于本节查询日志的输出依然借助Seilog。我们在Startup.cs类中Starup方法中是创建日志实例。 Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.RollingFile(Path.Combine( env.ContentRootPath, "{Date}.log")) .CreateLogger(); 接着我们只需要将Serilog注入到日志管道中即可在Configure方法中注入。 loggerFactory.AddSerilog(); 完成上述日志输出只需要安装如下三个包即可。 接下来记录日志只需要在控制器类或者其他类构造函数注入即可。 public class HomeController : Controller { private readonly ILogger _logger; private IBlogRepository _blogRepository; public HomeController(ILogger<HomeController> logger) { _logger = logger; _blogRepository = blogRepository; } } 关于EntityFramework Core中映射等就不再阐述,请参看前面EntityFramework Core系列。下面我们直接给出查询操作 public async Task<IEnumerable<Post>> GetPosts() { var posts = await _context.Blogs .AsNoTracking() .Include(d => d.Posts) .SelectMany(d => d.Posts) .Select(p => new Post() { Id = p.Id, Title = p.Title, Content = p.Content }).ToListAsync(); return await Task.FromResult(posts); } 不必太纠结上述查询语句,当有多表查询时我们最终需要获取Blog中的Post,最终才有了上述语句。我们看到如下日志文件。 我们来查看其中生成的Linq语句。 继续往下看我们将看到如下语句: 2017-09-28 00:36:58.901 +08:00 [Warning] The Include operation for navigation: 'd.Posts' was ignored because the target navigation is not reachable in the final query results. To configure this warning use the DbContextOptionsBuilder.ConfigureWarnings API (event id 'CoreEventId.IncludeIgnoredWarning'). ConfigureWarnings can be used when overriding the DbContext.OnConfiguring method or using AddDbContext on the application service provider. 上述警告语句提示导航属性Posts被忽略了因为其未能到达最终的查询结果,但是最终我们还是能看到里面确确实实是有数据的。 然后查看EntityFramework Core官方文档已经说明了此情况何时发生。 当利用饥饿加载进行查询操作时,最终并未返回原实体的实例此时将忽略Include导航属性。但是上述最终还是返回了数据,这是不是就暗示着并未利用饥饿加载而是在内存中操作呢。然后通过SQL Profiler进行监控得知只生成了一条SQL语句。 到这里还是没明白官方文档中所叙述的忽略导航属性究竟是什么意思?如果忽略了导航属性上述利用Linq进行查询应该会出现异常才对或者不会进行内连接,不知所云。所以上述查询我们只能返回Blog,而非其他实体,例如如下: public async Task<IEnumerable<Blog>> GetPosts() { var posts = await _context.Blogs .AsNoTracking() .Include(d => d.Posts) .ToListAsync(); return await Task.FromResult(posts); } 或者 public async Task<IEnumerable<Blog>> GetPosts() { var posts = await _context.Blogs .AsNoTracking() .Include(d => d.Posts) .Select(b => b) .ToListAsync(); return await Task.FromResult(posts); } 通过在github上找到如下issue:【https://github.com/aspnet/EntityFrameworkCore/issues/7153】 文中所述在1.1版本中将优化这种查询,目前我所使用版本为1.1.2,既然能正确返回值为何还打印警告日志提醒呢,看来这并不是问题,虽然忽略了但是还是进行了优化查询能够正确查询出数据。 总结 该问题演示在EntityFramework Core 1.1.2版本中,既然给出了提示那么应该是未解决,如果未解决那么将出现性能问题,如果我们进行投影然后ToList,此时利用Include进行饥饿加载,但是Include却被忽略,此时将生成一条单个SQL语句来查询获取结果集中每个元素的导航属性。若Include未被忽略并按照我们设想进行表连接,此时性能会更好。文中日志记录显示Include被忽略,但是生成SQL语句没有问题,却还是输出日志警告提醒,这究竟是为何,郁闷?