EF6 Code First下实现数据导入:直接SQL与存储过程的最佳实践咨询
ASP.NET MVC5 + EF6 Code First 数据导入方案选择与存储过程最佳实践
问题背景
我正在使用ASP.NET MVC 5和EF6 Code First(刚接触EF与MVC 5),需要从远程SQL Server数据库导入数据。
首先,我将远程视图的数据复制到本地数据库的表中,在Action方法中使用以下代码(变量名为意大利语):
using (var source = new TimeWebDBContext()) { using (var target = new GESTPrefContext()) { // 1 - Truncate table AnagraficaTimeWeb is exists target.Database.ExecuteSqlCommand("IF EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='AnagraficaTimeWeb') TRUNCATE TABLE AnagraficaTimeWeb"); // 2 - Copy from remote view to AnagraficaTimeWeb table var dati_importati = from i in source.VW_PREFNO_ANAGRAFICORUOLO select new AnagraficaTimeWeb() { Matricola = i.MATRICOLA, Cognome = i.COGNOME, Nome = i.NOME, Sesso = i.SESSO, Email = i.EMAIL, IdRuolo = i.IDRUOLO, Ruolo = i.RUOLO, DataInizio = i.DATAINIZIO, DataFine = i.DATAFINE, DataFineRapporto = i.DATALICENZ, DataUltimaImportazione = DateTime.Now }; target.DatiAnagraficaTimeWeb.AddRange(dati_importati.ToList()); target.SaveChanges(); } }
该远程视图返回员工及其角色列表,角色需导入到本地独立表PROFILO中,员工数据保存到IMPIEGATO表。
导入流程剩余步骤为:
- a)向
PROFILO表插入新数据(忽略已保存的数据) - b)更新本地
IMPIEGATO表中已存在的员工数据(覆盖姓名、邮箱等信息) - c)插入
IMPIEGATO表中尚未存在的新员工数据
由于刚接触EF6,考虑使用SQL代码实现,目前有两种方案:
方案1:Action方法中直接执行SQL
已编写对应步骤的SQL代码:
步骤a代码
StringBuilder sql = new StringBuilder(); sql.AppendLine("INSERT INTO PROFILO (IdTimeWeb, Descrizione, Ordinamento, Stato, Datainserimento)"); sql.AppendLine(" SELECT DISTINCT IdRuolo, Ruolo, 1," + ((int)EnumStato.Abilitato) + ",'" + DateTime.Now.ToShortDateString()+"'"); sql.AppendLine(" FROM AnagraficaTimeWeb i"); sql.AppendLine(" WHERE NOT EXISTS"); sql.AppendLine("(SELECT 1 FROM PROFILO p WHERE p.Descrizione = i.Ruolo)"); target.Database.ExecuteSqlCommand(sql.ToString());
步骤b代码
sql.Clear(); sql.Append("UPDATE i " + Environment.NewLine); sql.Append(" SET i.Cognome = a.Cognome" + Environment.NewLine); sql.Append(" , i.Nome = a.Nome" + Environment.NewLine); sql.Append(" , i.Sesso = a.Sesso" + Environment.NewLine); sql.Append(" ,i.Email = a.Email" + Environment.NewLine); sql.Append(" ,i.DataModifica = '" + DateTime.Now.ToShortDateString() +"'"+ Environment.NewLine); sql.Append(" FROM Impiegato i " + Environment.NewLine); sql.Append(" JOIN AnagraficaTimeWeb a on i.Matricola=a.Matricola " + Environment.NewLine); sql.Append(" WHERE i.Stato =" + ((int)EnumStato.Abilitato) + Environment.NewLine); target.Database.ExecuteSqlCommand(sql.ToString());
步骤c代码
sql.Clear(); sql.Append("INSERT INTO IMPIEGATO(Cognome, Nome, Matricola, Sesso, Email, Stato, DataInserimento) " + Environment.NewLine); sql.Append("SELECT a.Cognome" + Environment.NewLine); sql.Append(", a.Nome" + Environment.NewLine); sql.Append(", a.Matricola" + Environment.NewLine); sql.Append(", a.Sesso" + Environment.NewLine); sql.Append(", a.Email" + Environment.NewLine); sql.Append("," + ((int)EnumStato.Abilitato )+ Environment.NewLine); sql.Append(",'"+ DateTime.Now.ToShortDateString() +"'" + Environment.NewLine); sql.Append(" FROM AnagraficaTimeWeb a " + Environment.NewLine); sql.Append(" LEFT OUTER JOIN IMPIEGATO on a.Matricola = Impiegato.Matricola " + Environment.NewLine); sql.Append(" WHERE Impiegato.Matricola is null" + Environment.NewLine); target.Database.ExecuteSqlCommand(sql.ToString());
方案2:创建存储过程并在Action中调用
不清楚存储过程的创建方式:
- 2.a)在迁移的Up方法中创建?
- 2.b)还是直接在数据库中执行存储过程创建脚本,之后在Action方法中调用?
想咨询哪种方案更符合EF6 Code First的最佳实践,以及存储过程的正确创建方式。
方案对比与最佳实践建议
1. 方案选择:存储过程更贴合Code First最佳实践
直接在Action中拼接SQL字符串虽然能快速实现功能,但存在明显弊端:
- 维护性差:SQL逻辑分散在C#代码中,后续修改或调试需要跨语言查找;
- 可读性低:复杂SQL拼接后结构混乱,容易出现语法错误;
- 风险隐患:直接拼接日期、枚举值等存在SQL注入风险(即使当前场景参数可控,也不符合安全规范);
- 版本控制缺失:SQL逻辑无法和代码一起纳入版本管理,数据库变更难以追踪。
使用存储过程则能解决这些问题:
- 数据操作逻辑封装在数据库层,C#代码仅负责调用,职责划分更清晰;
- 存储过程可单独调试、优化,性能更可控;
- 结合EF迁移管理存储过程的创建/更新,能保证数据库结构与代码版本一致。
2. 存储过程的正确创建方式(Code First模式)
必须通过EF迁移的Up方法创建存储过程,而非直接在数据库中手动执行脚本,原因如下:
- Code First的核心是通过代码控制数据库结构,手动修改数据库会导致代码与数据库状态不一致;
- 迁移脚本可纳入版本控制,团队协作时能保证所有人的数据库结构同步;
- 后续修改存储过程时,只需新增迁移并更新Up/Down方法,即可实现平滑升级和回滚。
具体实现步骤:
- 生成空迁移:在Package Manager Console中执行命令:
Add-Migration CreateImportDataProcedures
- 在迁移的Up方法中编写存储过程创建脚本:
public override void Up() { // 创建导入PROFILO的存储过程 Sql(@" CREATE PROCEDURE dbo.ImportProfilo AS BEGIN INSERT INTO PROFILO (IdTimeWeb, Descrizione, Ordinamento, Stato, Datainserimento) SELECT DISTINCT IdRuolo, Ruolo, 1, @StatoAbilitato, GETDATE() FROM AnagraficaTimeWeb i WHERE NOT EXISTS (SELECT 1 FROM PROFILO p WHERE p.Descrizione = i.Ruolo) END ", new SqlParameter("@StatoAbilitato", (int)EnumStato.Abilitato)); // 创建更新Impiegato的存储过程 Sql(@" CREATE PROCEDURE dbo.UpdateImpiegato AS BEGIN UPDATE i SET i.Cognome = a.Cognome ,i.Nome = a.Nome ,i.Sesso = a.Sesso ,i.Email = a.Email ,i.DataModifica = GETDATE() FROM Impiegato i JOIN AnagraficaTimeWeb a on i.Matricola=a.Matricola WHERE i.Stato = @StatoAbilitato END ", new SqlParameter("@StatoAbilitato", (int)EnumStato.Abilitato)); // 创建插入Impiegato的存储过程 Sql(@" CREATE PROCEDURE dbo.InsertNewImpiegato AS BEGIN INSERT INTO IMPIEGATO(Cognome, Nome, Matricola, Sesso, Email, Stato, DataInserimento) SELECT a.Cognome ,a.Nome ,a.Matricola ,a.Sesso ,a.Email ,@StatoAbilitato ,GETDATE() FROM AnagraficaTimeWeb a LEFT OUTER JOIN IMPIEGATO on a.Matricola = Impiegato.Matricola WHERE Impiegato.Matricola is null END ", new SqlParameter("@StatoAbilitato", (int)EnumStato.Abilitato)); }
- 在Down方法中添加删除存储过程的逻辑(用于回滚):
public override void Down() { Sql("DROP PROCEDURE dbo.ImportProfilo"); Sql("DROP PROCEDURE dbo.UpdateImpiegato"); Sql("DROP PROCEDURE dbo.InsertNewImpiegato"); }
- 执行迁移:在Package Manager Console中执行:
Update-Database
- 在Action方法中调用存储过程:
using (var target = new GESTPrefContext()) { // 调用存储过程 target.Database.ExecuteSqlCommand("EXEC dbo.ImportProfilo"); target.Database.ExecuteSqlCommand("EXEC dbo.UpdateImpiegato"); target.Database.ExecuteSqlCommand("EXEC dbo.InsertNewImpiegato"); }
额外优化建议:
- 把三个存储过程合并为一个
ImportAllData存储过程,减少数据库调用次数; - 使用
GETDATE()代替C#中的DateTime.Now,避免客户端与数据库时间不一致的问题; - 存储过程中始终使用参数化查询,保持代码规范。
内容的提问来源于stack exchange,提问作者Liuc
相关产品推荐
相关产品推荐

