老周不经意间翻了一下博客列表,上一篇水文竟然在 一个月前。啊,大海啊,全是水;时间啊,你跑得真快!过了一个月老周竟没感觉出来,可是这一个月里,好像啥事也没干成,就改了一下几个旧项目。也许是现在大环境真的不好,新项目不好找。新的活有是有,比较简单,却很奇怪,比那货难的项目都做过,偏偏这货没头绪。这东西需求就是画图——是用程序来画图,类似甘特图。莫名其妙的是,这活会卡在窗口排版上。按照需求,这货窗口特多。点一下这里,弹一个对话框出来可以修改;点一下那里,又要弹一个出来允许修改;右键单击一下,弹出上百个选项的菜单(Check 模式),简直离谱,那么多指标项,我都怀疑他们老板根本不会去看的,但他们非要做全面。更闹心的是主窗口,想想现在的显示屏分辨率又高又大,一个窗口全屏放在那里,可上面又没几个控件,70% 的地方就是画进度图,每几秒刷新一下。虽然简单易用最好,但这窗口是看着空洞了一些。免得那些不懂编程的人说老周这项目根本没干啥活,老周还打算给它弄个高清《美少女战士》或《刘姥姥进大观园》当背景图,这样看起来就不空洞了。
今天咱们聊一个很单的主题,写一个 Web API,客户端可以调用它来备份 SQL Server 数据库。不知道大伙伴们有没有做过这活。相信做过的人会比老周更明白,毕竟,老熟人都知道,老周有两大弱项:SQL、汇编。汇编呢,是学生时代没好好学,想当年很轻松地就拿下了二级C++,偏就没学会汇编;而 SQL 呢,本来就学得一般,再加上用得少,忘得差不多了,所以别人给老周安排的项目基本不包括写 SQL 的,最终导致 SQL 方面越来越弱。
EF Core 不仅能用 LINQ 和实体类型配合操作,确实能让你在80%的情况下不用写SQL语句,但,为了灵活,EF Core 和早期 EF 和 ADO.NET 一样,是可以直接执行 SQL 语句的。这意味着,备份和还原数据库不在话下。
这个功能的实现并不难,但有两小坑,老周接下来会慢慢讲。
备份数据库用的是 BACKUP DATABASE 语句,比如这样- BACKUP DATABASE <你的数据库名>
- TO DISK = 'D:\backups\dbs.bak'
复制代码 DISK 后的表达式不一定指定磁盘,更多时候是备份的文件名。如果还要备份日志,可以接着执行 BACKUP LOG 语句。- BACKUP LOG <数据库名> TO DISK = 'E:\WrtMsn\oodo-log.bak'
复制代码 如果要把备份放到另一台服务器上,可以用共享路径。- BACKUP DATABASE <数据库名> TO DISK = '\\GaoXServer\shares\team\abc.bak'
复制代码 这里就有第一个坑——权限,不管是你的程序进程还是 MSSQL 进程,有些目录是没有写入的权限的。比较懒的做法是在放置备份的目录上给个 everyone 的完全控制权限。老周不推荐这样,太不厚道了。若是不太好确定用哪个用户(毕竟有时候不一定是某个用户,而是系统服务),Windows 里面有一个好用的名称,叫 Authenticated Users,看字面意思就已经过验证的用户,就不是单指某个用户了。这个比 everyone 靠谱多了,起码匿名的不允许。
简单说说操作,在目录上右击,打开“属性”窗口,切换到“安全”页。点击下面的“高级”按钮。
点击“添加”按钮。
点击“选择主体”,找到 Authenticated Users 并添加。最起码给予写入和修改权限。
然后一路确定、应用即可。
============================================================================================
好了,理论知识完毕,下面可以动手了。
先建个库用来测试。这里老周建了个实体,名为 Movie,表示一部大片。- public class Movie
- {
- /// <summary>
- /// 编号
- /// </summary>
- public int Id { get; set; }
- /// <summary>
- /// 电影标题
- /// </summary>
- public string? Subject { get; set; }
- /// <summary>
- /// 导演是谁
- /// </summary>
- public string? Director { get; set; }
- /// <summary>
- /// 哪年上映的
- /// </summary>
- public int? Year { get; set; }
- /// <summary>
- /// 讲了啥故事
- /// </summary>
- public string? Desc { get; set; }
- }
复制代码 接下来写 DbContext。- public class TestDBContext:DbContext
- {
- public TestDBContext(DbContextOptions<TestDBContext> options)
- : base(options)
- { }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- // 主键
- modelBuilder.Entity<Movie>().HasKey(x => x.Id);
- // 影片名为必填
- modelBuilder.Entity<Movie>().Property(x => x.Subject).IsRequired();
- // 填充初始数据
- modelBuilder.Entity<Movie>().ToTable("t_movies").HasData(
- new Movie { Id = 1, Subject = "狗二总裁", Year = 2026, Director = "大头苏", Desc = "二五仔二次创业的励志故事" },
- new Movie { Id = 2, Subject = "子夜实验室", Year = 2025, Director = "丁小丁", Desc = "某大学的实验室总是在子夜时分莫名发生火灾,校方怀疑有人恶意纵火,于是,学校成立专项小组进行调查……" }
- );
- // 下面这行可以省略,Id 属性默认是自增长标识
- //modelBuilder.Entity<Movie>().Property(c => c.Id).ValueGeneratedOnAdd();
- }
- public DbSet<Movie> Movies { get; set; }
- }
复制代码 重写 OnModelCreating 方法是对模型做一些自定义设置,如果你只需保留默认,可以不重写此方法。由于表示主键的属性名为 Id,EF Core 会自动认为是主键,且启用自增长标识。上面代码老周用 ToTable 方法映射到的数据表名 t_movies。如果你要求表名和实体类名一样,那可以忽略。
另外,代码还调用了 HasData 方法,其用途是插入一些初始数据。如果你的数据表的初始状态允许空白,可以忽略。如果要插入种子数据(即初始数据),这里有第二个坑,一定要显式地指定 Id 属性的值,这里比较特殊。因为这是种子数据,必须保证每个字段的值是静态的,也就是说,不管你的程序在哪台机器上运行,得保证插入的数据是相同的。Id 字段虽然是自增长的,可你无法保证实际使用时数据库会从1开始增长,也不保证步长值一定是1。说不定人家是 100、101、102 呢。正因为如此,如果你忽略 Id 的值就会抛出异常。有一种方案就是使用负值,这个不如硬编码一个整数值保险一些。
完工后,要在 ASP.NET Core 应用程序的服务容器中注册。- var builder = WebApplication.CreateBuilder(args);
- builder.Services.AddControllersWithViews();
- // 配置数据库
- string? connectionStr = builder.Configuration.GetConnectionString("prj_cns");
- builder.Services.AddDbContext<TestDBContext>(ob =>
- {
- ob.UseSqlServer(connectionStr);
- });
- var app = builder.Build();
复制代码 数据库的连接字符串在配置文件中(appsettings.json)。- {
- "Logging": {
- ……
- }
- },
- "AllowedHosts": "*",
- "<em><strong>ConnectionStrings</strong></em>": {
- "prj_cns": "Data Source=.\\SQLTEST;Initial Catalog=Demo;Integrated Security=True;Persist Security Info=False;Pooling=False;Encrypt=True;Trust Server Certificate=True"
- }
- }
复制代码 在配置文件中使用 ConnectionStrings 节点有特殊含义,把数据库连接字符串放在此节点下,代码中获取时可以简化一些。- builder.Configuration.GetConnectionString("prj_cns");
复制代码 就是 GetSection("ConnectionStrings")["prj_cns"] 的简化版。
======================================================================================
最后要实现的是 Web API。对于有明确的模块功能的代码我们尽量用 MVC Controller 去实现,Mini-API 只适合没啥模块化的简单代码,也避免把 Main 方法的代码搞得又长又臭。
[code]public class TestController : Controller{ private readonly TestDBContext _db; public TestController(TestDBContext db) { _db = db; } [HttpGet("/")] public ActionResult Index() { _db.Database.EnsureCreated(); return View("~/Views/home.cshtml", _db.Movies.ToArray()); } [HttpPost("/demo/new")] public async Task NewData([FromBody]Movie mvi) { bool hasany = await _db.Movies.AnyAsync(m => m.Subject!.Equals(mvi.Subject)); if(hasany) { return Json( new { Code = 2, Message = "此电影已存在" } ); } await _db.Movies.AddAsync(mvi); int i = await _db.SaveChangesAsync(); if(i |