写过几个小项目的朋友可能都遇到过这个问题:用户点个“删除订单”,代码里直接 new 了个 SqlConnection,拼了条 DELETE SQL 就执行了——这事儿看起来跑得挺顺,但下次加个日志、换个数据库、或者要测逻辑的时候,就容易卡壳。
先说答案:数据库操作属于 Model 层
不是 Controller,也不是 View,更不是硬塞进 Controller 里的一个私有方法。Model 层不单是几个实体类(比如 User.cs、Order.cs),它还负责和数据打交道:查、增、改、删、事务控制、连接管理……这些活儿,都该由 Model 或它的子模块来扛。
举个接地气的例子:你开个小网店后台,用户点击「查看最近10笔订单」。Controller 只做三件事:
① 拿到用户 ID(比如从 Session 或 Token 解出来);
② 调 Model 层的一个方法,比如 orderService.GetRecentOrders(userId, 10);
③ 把结果塞给 View 渲染。
至于这个 GetRecentOrders 方法内部怎么连数据库、用 EF 还是 Dapper、要不要缓存、要不要分页参数校验——Controller 一概不管。
那 Model 层具体长啥样?
常见的结构是这样:
Models/
├── Order.cs // 实体类
├── IOrderRepository.cs // 接口,定义“能干啥”
└── SqlOrderRepository.cs // 实现类,真连数据库、写 SQL/Dapper/EF接口 IOrderRepository 声明:
public interface IOrderRepository
{
List<Order> GetRecentOrders(int userId, int count);
bool DeleteById(int id);
}实现类 SqlOrderRepository 里才出现 ConnectionString、SqlCommand、using(var conn = ...) 这些东西——它只对 Interface 负责,不对 Controller 负责。
为什么不能写在 Controller 里?
试想一下:某天老板说“订单数据要从 SQL Server 迁到 PostgreSQL”。如果你所有数据库代码都在 Controller 里,就得挨个翻十几个 Action,改连接、换驱动、调语法……改完还得祈祷没漏掉哪个 try-catch 里的 conn.Close()。
而如果数据库逻辑全在 Repository 里,你只要新写一个 PgOrderRepository,在 DI 容器里把接口映射一换:
services.AddScoped<IOrderRepository, PgOrderRepository>();完事。Controller 和 View 一行都不用动。
顺便说说常见误区
• “我用 EF 的 DbContext 直接在 Controller 里查,这不也是 Model 吗?”
——不,这是把 Model 的职责撕碎了贴在 Controller 上。DbContext 是工具,不是业务逻辑的容器。
• “我把 SQL 写在 View 里用 @functions{} 执行…”
——快打住!View 只管展示,连数据库会出大事,轻则报错乱码,重则 SQL 注入直接丢库。
• “那 Service 层算哪层?”
——Service 层其实是 Model 的延伸,处理跨多个 Repository 的业务规则(比如“扣库存+写订单+发消息”要一起成功或失败),它调用 Repository,自己不碰 Connection。
一句话记住:谁跟数据库说话,谁就是 Model 的人;Controller 只传话,View 只看话。