0%

EntirtyFramework框架是一个轻量级的可扩展版本的流行实体框架数据访问技术,ORM工具(Object Relational Mapping 对象关系映射)

EF有三种使用场景,1. 从数据库生成Class(DB First),2.由实体类生成数据库表结构(Code First),3. 通过数据库可视化设计器设计数据库,同时生成实体类(Model First)。

实体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>();
}
}

public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }

public List<Post> Posts { get; set; }
}
// 从模型中排除的类型
[NotMapped]
public class BlogMetadata
{
public DateTime LoadedFromDatabase { get; set; }
}

按照约定,每个实体类型将设置为映射到与公开实体的 DbSet 属性同名的数据库表。 如果给定实体不存在 DbSet,则使用类名称。或使用注解 [ Table(“tableName”) ]

实体属性

1
2
3
4
5
6
7
8
9
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }

//从模型中排除的属性
[NotMapped]
public DateTime LoadedFromDatabase { get; set; }
}

列注解:

  • 列名 [ Column(“blog_id”) ]
  • 数据类型 [Column(TypeName = “varchar(200)”)]
  • 校验 [ MaxLength(500) ] [ Required ]

主键

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasKey(b => b.BlogId)
.HasName("PrimaryKey_BlogId");
}

Alternative Key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogUrl)
.HasPrincipalKey(b => b.Url);
}
}

策略维护 多对多关系

官网Doc

目前尚不支持多对多关系,没有实体类来表示联接表。 但是,您可以通过包含联接表的实体类并映射两个不同的一对多关系,来表示多对多关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MyContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostTag>()
.HasKey(t => new { t.PostId, t.TagId });

modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);

modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}
}

public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public List<PostTag> PostTags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
public int PostId { get; set; }
public Post Post { get; set; }

public string TagId { get; set; }
public Tag Tag { get; set; }
}

一对多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 博客
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }

public List<Post> Posts { get; set; }
}
// 文章
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public int BlogId { get; set; }
public Blog Blog { get; set; }
}
  • Post是依赖实体
  • Blog是主体实体
  • Blog.BlogId是主体键(在本例中为主密钥,而不是备用密钥)
  • Post.BlogId为外键
  • Post.Blog是一个引用导航属性
  • Blog.Posts是集合导航属性
  • Post.Blog是的反向导航属性

    一对一

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Blog
    {
    public int BlogId { get; set; }
    public string Url { get; set; }

    public BlogImage BlogImage { get; set; }
    }

    public class BlogImage
    {
    public int BlogImageId { get; set; }
    public byte[] Image { get; set; }
    public string Caption { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
    }
    配置关系时HasForeignKey必须指定实体类型,这一点区别于上面的书写方式
    1
    2
    3
    4
    modelBuilder.Entity<Blog>()
    .HasOne(b => b.BlogImage)
    .WithOne(i => i.Blog)
    .HasForeignKey<BlogImage>(b => b.BlogForeignKey);

    关联关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class MyContext : DbContext
    {
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    modelBuilder.Entity<Post>()
    .HasOne<Blog>()
    .WithMany()
    .HasForeignKey(p => p.BlogId);
    }
    }
    关联关系有Reqiuired和Optional之分,前者的情况下,对主体实体的删除会导致依赖实体被级联删除,而对于后者,默认不被配置为级联删除而将外键属性置为null

    关联查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public async Task<IActionResult> Details(string code)
    {
    if (code == null)
    {
    return NotFound();
    }
    // Blog <>--- ReferenceMap ---<> Theme
    var Blog = await _context. Blogs
    .Include(blog => blog.ReferenceMap)
    .ThenInclude(map => map.Theme)
    .FirstOrDefaultAsync(m => m.Code == code);
    if (Blog == null)
    {
    return NotFound();
    }

    return View(Blog);
    }
    Inclulde 被称为 预先加载 eager load
    使用ThenInclude关联多个层次
    Include可以包含过滤, 如下取得曾发表带’敏感’词标题文章的所有博客,以及相应的文章
    1
    var restrictBlogs = _context.Blogs.Include(blog => blog.Posts.Where(post => post.Title.Contains("敏感"))).ToList()

显式加载(explicit load)

1
2
3
var blog = _context.Blogs.Single(blog=>blog.Author=="QQs") // 此处关联属性Posts为null
...
var posts = _context.Entry(blog).Collection(blog => blog.Posts).Query().Where(post => post.Title.Contains("敏感")).ToList() // 此时blog对象的Posts属性被填充(仅过滤结果)

不返回结果posts可以直接Load
1
_context.Entry(blog).Collection(blog => blog.Posts).Load()

QQs:私以为这与预先加载并无多大区别

延迟加载(lazy load)
Microsoft Docs:相关数据的延迟加载

关联存储

here
向导航属性(blog.Posts)中添加新实体,EF自动发现关联实体并将其插入数据库

1
2
3
4
5
6
7
8
await using (var context = new BloggingContext())
{
var blog = await context.Blogs.Include(b => b.Posts).FirstAsync();
var post = new Post { Title = "Intro to EF Core" };

blog.Posts.Add(post);
await context.SaveChangesAsync();
}

自动更改外键列
1
2
3
4
5
6
7
8
await using (var context = new BloggingContext())
{
var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" };
var post = await context.Posts.FirstAsync();

post.Blog = blog;
await context.SaveChangesAsync();
}

上面的代码没有显式操作外键post.blogId,但EF会自动更新,并且将所需的新实体blog插入数据库

CRUD

使用数据库上下文修改模型(包括新增和移除),并执行SaveChanges,相当于commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// create
context.Add(new Student{
FirstName="Jack",
SurName="Ma"
});
context.SaveChanges();
// select
var MaYun = context.Students
.Where(s => s.FirstName == GetName()).ToList();
// update
MaYun.Age=8;
context.Update(MaYun)
context.SaveChanges()
// delete
context.Remove(MaYun);
context.SaveChanges()

一般在web应用中使用异步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// create
context.Add(new Student{
FirstName="Jack",
SurName="Ma"
});
context.SaveChangesAsync();
// select
var MaYun = context.Students
.Where(s => s.FirstName == GetName()).ToListAsync();
// update
MaYun.Age=8;
context.SaveChangesAsync()
// delete
context.Remove(MaYun);
context.SaveChangesAsync()

Caution! 在对DbContext的多次操作中,如果前面一次SaveChanges出错,如Add操作违反唯一约束而失败,需要将该实体类实例从DbContext中移除(或修正),否则出错的命令会一直在提交队列中,反复报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try
{
_context.Add(newBlog);
await _context.SaveChangesAsync();
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException dbEx)
{
// TODO the result description is not properly rigorous.
if (null != dbEx.InnerException
&& dbEx.InnerException.Message.Contains("constraint"))
{
System.Console.WriteLine("Blog exists;");
}
else
{
// other error handle
}
_context.Remove(newBlog);
}

DBContext.Add成功后 若主键id使用数据库策略生成 Add成功后即可从对象中取到

DBcontext和connectionstring

startup.cs

1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DBConnection")));
}

重连

数据库如SQL Server的provider程序,可以识别可重试(retry)的异常类型

1
2
3
4
5
6
7
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<PicnicContext>(
options => options.UseSqlServer(
Configuration.GetConnectionString("DBConnection"),
providerOptions => providerOptions.EnableRetryOnFailure()));
}

事务

额外的,Transaction commit failure

对于多项实体操作(CRUD)后SaveChanges,SaveChanges是事务性的,意味着前面所有操作成功或失败,而不会产生部分成功部分失败

Lazy load

访问导航属性(外键)时再次查询数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using (var dbContext = new CategoryEntities())
{
dbContext.Configuration.LazyLoadingEnabled = true; // 默认是true,针对导航属性
var categoryList = dbContext.Set<Category>().Where(p => p.CategoryId == 3);
// 只会在数据库里面查询Category表,不会查询ProductDetail表
foreach(var category in categoryList)
{
Console.WriteLine("CategoryId:"+category.CategoryId+ ",CategoryName:"+category.CategoryName);
// 这时才会去数据库查询ProductDetail表
foreach (var product in category.ProductDetails)
{
Console.WriteLine("ProductName:"+product.ProductName);
}
}
}

不再继续查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using (var dbContext = new CategoryEntities())
{
dbContext.Configuration.LazyLoadingEnabled = false; // 不延迟加载,不会再次查询了
var categoryList = dbContext.Set<Category>().Where(p => p.CategoryId == 3);
// 只会在数据库里面查询Category表,不会查询ProductDetail表
foreach (var category in categoryList)
{
Console.WriteLine("CategoryId:" + category.CategoryId + ",CategoryName:" + category.CategoryName);
// 这时不会去数据库查询了,所以用户全是空的
foreach (var product in category.ProductDetails)
{
Console.WriteLine("ProductName:" + product.ProductName);
}
}
}

一次性完成查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 显示加载
using (var dbContext = new CategoryEntities())
{
// 不延迟加载,指定Include,一次性加载主表和从表的所有数据
var categoryList = dbContext.Set<Category>().Include("ProductDetails").Where(p => p.CategoryId == 3);
foreach (var category in categoryList)
{
Console.WriteLine("CategoryId:" + category.CategoryId + ",CategoryName:" + category.CategoryName);
// 不会再查询
foreach (var product in category.ProductDetails)
{
Console.WriteLine("ProductName:" + product.ProductName);
}
}
}

issue: Data is Null. This method or property cannot be called on Null values.

一个很简单的出错原因是model的基本类型(非对象,不能设置为null)如int,Guid等的属性,其对应的table field为null。
应以int?,Guid?作为属性类型以支持null

Parent/Child

对应于使用id,parentid组织的父子关系表,常见的组织机构树,职能头衔树等

1
2
3
4
5
6
7
8
9
public class Group
{
public Guid ID { get; set; }
public string Name { get; set; }
public Guid? ParentID { get; set; }
public Group Parent { get; set; }

public ICollection<Group> Children { get; } = new List<Group>();
}

查询子树
1
2
3
var data = (await _context.Group.ToListAsync())
.Where(g => g.ID == new Guid(groupId))
.ToList();

Hierarchy Data

参考Using SQL Server HierarchyId with Entity Framework Core
package:

  • Microsoft.EntityFrameworkCore.SqlServer
  • EntityFrameworkCore.SqlServer.HierarchyId
    数据库上下文需要配置启用HierarchyId,否则出现下述异常
    The property is of type ‘HierarchyId’ which is not supported by current database provider. Either change the property CLR type or ignore the property using the ‘[NotMapped]’ attribute or by using ‘EntityTypeBuilder.Ignore’ in ‘OnModelCreating’.

在Startup.cs,配置启用HierarchyId

1
2
3
4
5
6
7
8
9
10
11
 public void ConfigureServices(IServiceCollection services)
{
...
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DataContext"), conf=>
{
conf.UseHierarchyId();
}
));
...
}

model定义
1
2
3
4
5
6
7
8
public class Group
{
public Guid ID { get; set; }
public string Name { get; set; }
public HierarchyId GroupLevel { get; set; }

public ICollection<Group> Children { get; } = new List<Group>();
}

查询linq
1
2
3
4
5
6
7
8
9
public async Task<List<Group>> GetChildrenByGroupIDAsync(Guid groupID)
{
Group self = await _context.Groups.FindAsync(groupID);
List<Group> groups = await _context.Groups
.Where(g => g.GroupLevel.IsDescendantOf(self.GroupLevel))
.ToListAsync();

return groups; //.FindAll(g => g.ID != groupID);
}

其他查询见文章SQLServer

Transient Error

Exception: An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding ‘EnableRetryOnFailure()’ to the ‘UseSqlServer’ call.

StackOverflow: Getting transient errors when making calls against Azure SQL Database from Azure Function

数据库系统偶现Transient Error,这种暂时性错误的根本原因(underlying cause)很快就能自行解决,且在错误抛出时,.net程序会抛出上述的SqlException,为了处理这些错误,可应用程序代码中实现重试逻辑,而不是以应用程序错误的形式呈现给用户。
在Startup.cs,配置启用RetryOnFailure

1
2
3
4
5
6
7
8
9
10
11
 public void ConfigureServices(IServiceCollection services)
{
...
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DataContext"), conf=>
{
conf.EnableRetryOnFailure();
}
));
...
}

但是这里有个bug System.ArgumentException thrown when EnableRetryOnFailure is used. 该bug已在dotNet 5版本修复

分页

1
2
List<customers> _customers = (from a in db.customers select a).ToList();
var _dataToWebPage = _customers.Skip(50).Take(50);

StackOverflow:C# entity framework pagination

ORM注意事项

EF O/RM 注意事项

日志

startup.cs

1
2
3
4
5
6
7
services.AddDbContext<MyDBContext>(options => {
options.UseSqlServer(Configuration.GetConnectionString("MyDBContext"), conf => {
conf.UseHierarchyId();
conf.EnableRetryOnFailure();
});
options.LogTo(System.Console.WriteLine);
});

Docs:简单的日志记录
日志委托和日志级别:
1
options.LogTo(Console.WriteLine, LogLevel.Information;

Exception:this sqltransaction has completed it is no longer usable

1
2
3
4
5
6
7
8
9
10
11
[HttpPost]
[Route("api/[controller]/create")]
public async Task<Result> Create([FromBody] QModel model){
...
try{
_context.Add(model);
await _context.SaveChangesAsync();
}catch(Exception ex){
// ...
}
}

如上一个insert数据的接口,在有限的并发条件下(也就是for循环几条请求),个别错误数据可以造成其他正常数据插入失败
报 this sqltransaction has completed it is no longer usable 以及 zombie check等解释
查了2天资料未能解决
次日反思 问题或许出在多条线程同时向数据库上下文中推数据(即_context.Add)其中一个错误数据的出错,事务自动回滚,导致了其他线程中访问该事务已不可用。
此处使用_context.AddAsync方法可以解决
即,该问题就是个ef方法的线程安全的问题,可见StackOverflow: AddAsync vs Add
那么我一个需求要添加user并为其分配新的group,一个事务里两个add操作怎么办呢,另起一小节:

一个事务多个操作的线程安全

其实在官方文档最初的概述中,强调了DbContext的线程不安全 见Microsoft Docs: DBContext Lifetime

DbContext 不是线程安全的。 不要在线程之间共享上下文。 请确保在继续使用上下文实例之前,等待所有异步调用。

关于以事务作为上下文生命周期的配置, 见StackOverflow:Configuring Dbcontext as Transient 然而!经实践同一接口的并发测试 仍然会出现this sqltransaction has completed it is no longer usable的异常
依赖注入的DBContext
在Startup的ConfigureServices中注册MyDBContext服务提供程序:

1
2
3
4
5
services.AddDbContext<MyDBContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("MyDBContext"),
conf => { conf.UseHierarchyId();
conf.EnableRetryOnFailure();
}), ServiceLifetime.Transient);

注入DBContext:
1
2
3
4
5
6
7
8
9
10
11
12
public class GroupsController : ControllerBase
{
private readonly MyDBContext _context;
private IGroupTreeService _groupTreeService;

public GroupsController(MyDBContext context, IGroupTreeService groupTreeService)
{
_context = context;
_groupTreeService = groupTreeService;
}
...
}

控制DBContext生命周期在函数内部
官方Doc:启用RetryOnFailue情形下的手动事务方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface IWorker
{
void DoWork(Func<MyDbContext> dbFactory);
}

public class WorkerRunner
{
private readonly DbContextOptions<MyDbContext> _dbOptions;

private readonly List<IWorker> _workers;

public WorkerRunner(DbContextOptions<MyDbContext> dbOptions, List<IWorker> workers)
{
_dbOptions = dbOptions;
_workers = workers;
}

public void RunWorkers()
{
using (var context = new MyDbContext(_dbOptions))
{
using (var tran = context.Database.BeginTransaction())
{
foreach (var worker in _workers)
worker.DoWork(() =>
{
// This won't work
var db = new MyDbContext(_dbOptions);
// And this one will even throw exception when used with in-memory database (during unit testing)
db.Database.UseTransaction(tran.GetDbTransaction());
return context;
});

tran.Commit();
}
}
}
}

就是new一个DBContext用, Caution! 经测单靠new DBContext不能阻止transaction Error

自动生成id

1
2
3
4
5
6
[Table("User")]
public class User{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
}

默认值

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Property(e => e.Role)
.HasDefaultValue(1);
}

命令

1
2
sqlcmd -S .\SqlExpress
NET START/PAUSE/CONTINUE/STOP/ MSSQLSERVER

QQs未能成功实践。。
或者,任务管理器手动启动 MSSQL$SQLEXPRESS

schema

在MySQL中schema的概念和database一致

但是微软搞什么都要多加点概念,sqlserver中,表名前带有schema标记如dbo.table1,这里的dbo指数据库的默认用户database owner

导出表结构(create table)语句时会带着schema

1
2
3
4
5
6
7
8
create table [ent].[tabletemp](
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](50) NULL,
PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

迁移时执行该语句会提示”The specified schema name “env” either does not exist or you do not have permission to use it.”

创建schema
1
create schema ent

变更schema
1
ALTER SCHEMA ent TRANSFER OBJECT::dbo.table1;  

创建新用户及授权访问

参考原文

  • 配置登录名 Database Server —> Security —> Logins —> 右键New Login
  • 常规General标签页中,配置认证方式等
  • 服务器角色(Server Roles)添加 public sysadmon
  • 用户映射(User Mapping)添加创建的新用户
  • 安全对象(Securable)搜索 —> 选择 The Server(当前数据库服务器名)
  • 状态默认

ServerName

sqlserver实例默认以计算机名+服务提供者命名,如SHAL400/SQLEXPRESS, 甚至用ip代替计算机名都会导致无法连接.

配置sqlserver支持远程访问:

  1. 从本地SSMS连接数据库,右键服务器—Facets—Server Configuration—RemoteAccessEnable=true
    sqlserver_remote_access
  2. 打开SQL Server Configuration Manager(SSCM) SQL Server Browser Running,
  3. SSCM—SQL Server Network Configuration—Protocols for SQLEXPRESS—TCP/IP Enable, 然后右键打开Properties设置ip及端口如下(注意IPAll的TCP Dynamic Ports不要写死)
    sqlserver_remote_access_tcpip
  4. 配置防火墙略

调用存储过程

1
EXEC storedProcedure1 @param='01'

约束Constraint

1
2
3
4
5
6
7
8
9
CREATE TABLE [dbo].[Group](
[ID] [uniqueidentifier] NOT NULL,
[CreateTime] [datetime2](7) NULL,
[Name] [nvarchar](80) NULL,
[Valid] [bit] NOT NULL,
[UpdateTime] [datetime2](7) NULL,
[Comment] [nvarchar](500) NULL,
CONSTRAINT [AK_Group_Name] UNIQUE ([Name])
) ON [PRIMARY]

关于大小写

据说sqlserver 安装过程中有是否区分大小写的选项,默认情况下无论表名、列名、字段、参数都不区分大小写,更过分的是查询条件的值也不区分————where name=’abc’和where name=’AbC’是一样的结果。如果要区分查询条件的大小写,中文网络上建议如下例子,追加条件

1
select * from table1 where name='abc' collate Chinese_PRC_CS_AI_WS 

Chinese_PRC_CS_AI_WS实际表示中国大陆UNICODE字符集规则(Chinese PRC),区分大小写(Case Sensitive,CS),不区分重音(Accent Insensitive,AI),区分宽度(Width Sensitive,WS,半角/全角字符受此条件影响)

类似的还有
1
2
SQL_Latin1_General_CP1_CS_AI
Latin1_General_CS_AI

查询当前默认规则
1
SELECT SERVERPROPERTY(N'Collation')

查询支持的字符集规则
1
SELECT * from ::fn_helpcollations()

内置对象的表

  • sys.schemas

  • 执行历史

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    SELECT TOP 1000 QS.creation_time, 
    SUBSTRING(ST.text,
    (QS.statement_start_offset / 2) + 1,
    ((CASE QS.statement_end_offset
    WHEN - 1 THEN DATALENGTH(st.text)
    ELSE QS.statement_end_offset
    END - QS.statement_start_offset) / 2) + 1)
    AS statement_text,
    ST.text,
    QS.total_worker_time,
    QS.last_worker_time,
    QS.max_worker_time,
    QS.min_worker_time
    FROM
    sys.dm_exec_query_stats QS CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) ST
    WHERE 1=1

    edit data

    SSIS提供了Edit Top 200 Rows,但是写入表格内容各种格式不正确,宜Script Table to…Insert to

    Guid用NEWID(),时间就用SYSDATETIME()

    STUFF

1
STUFF ( character_expression , start , length , character_expression )

CAST & CONVERT

数据类型转换

1
2
SELECT CAST(t1.num AS varchar) from t1;
SELECT CONVERT(varchar, t1.num) from t1;

将自然键替换为人工键

原实体以序列号为主键,现添加ID列并填充GUID

1
2
3
ALTER TABLE dbo.Table1 DROP CONSTRAINT PK_Table1 // 移除原主键
ALTER TABLE dbo.Table1 DROP COLUMN SerialNumber // 移除列
ALTER TABLE dbo.Table1 ADD ID uniqueidentifier NOT NULL default newID()

exception The object ‘DFTable1ID__34C8D9D1’ is dependent on column ‘ID’. ALTER TABLE DROP COLUMN failed because one or more objects access this column

ID作为列名会默认添加CONSTRAINT,如上所提及的DFTable1ID34C8D9D1 因此要删除这个ID列需要先 ALTER TABLE dbo.Table1 DROP CONSTRAINT DFTable1ID34C8D9D1

层次结构数据

具有父级、子级关系的层次结构数据
Oracle的递归查询语法:

1
select  * from t_dw CONNECT BY PRIOR id = parentID START WITH id='dw001'

SqlServer中没有上述语法,而使用内置hierarchyid简化层次结构数据的存储和查询,

https://www.meziantou.net/using-hierarchyid-with-entity-framework-core.htm

1
2
3
4
5
6
7
-- 根节点 /
update t_dw set orgLvl=HierarchyID::GetRoot() where parentID is null
-- 子树 /1/,/2/
update t_dw set orgLvl=HierarchyID::Parse('/1/') where name='dw1'
update t_dw set orgLvl=HierarchyID::Parse('/2/') where name='dw2'
-- 叶 /1/3/
update t_dw set orgLvl=HierarchyID::Parse('/1/1/') where name='dw1-a'

插入

1
2
3
insert t_dw (id,name,ParentID,orgLvl) 
values(newid(),'dw1-b','xxxxxxxxxxxxxxx',
HierarchyID::Parse('/1/').GetDescendant(CAST('/1/1/' AS hierarchyid), NULL))

得到/1/2/ dw1-b 即在/1/的子节点,左树为/1/1/右树为null位置插入新节点

层级

1
SELECT CAST('/1/2/' AS hierarchyid).GetLevel() -- 结果:2

后代
1
2
3
SELECT name, orgLvl.ToString()
FROM t_dw
WHERE orgLvl.IsDescendantOf(CAST('/1/' AS hierarchyid)) = 1

IsDescendant为1(表示true)返回所有后代(实际上也包括‘/1/’自己), 0返回所有非后代(父代,sibling树)
1
2
3
SELECT name, orgLvl.ToString()
FROM t_dw
WHERE orgLvl.GetAncestor(2) = HierarchyID::Parse('/1/')

GetAncestor返回指定层级的后代,参数为层级:0返回‘/1/’自己;1返回所有子节点,2返回所有孙子节点

移动

1
2
3
4
5
6
7
8
DECLARE @CurrentNode hierarchyid , @OldParent hierarchyid, @NewParent hierarchyid 
select @CurrentNode=orgLvl from t_dw where name='dw_x'; -- /1/1/
select @OldParent=orgLvl from t_dw where name='dw_old'; -- /1/
select @NewParent=orgLvl from t_dw where name='dw_new'; -- /3/
UPDATE t_dw
SET OrgNode = @CurrentNode.GetReparentedValue(@OldParent, @NewParent)
WHERE OrgNode = @CurrentNode ; -- /3/1/
GO

其他进阶操作:
查找祖先
列出祖先)
移动子树

获取每个表的数据条数

1
2
3
select schema_name(t.schema_id) as [Schema], t.name as TableName,i.rows as [RowCount] 
from sys.tables as t, sysindexes as i
where t.object_id = i.id and i.indid <=1

按rownumber删除

1
2
3
4
; with cte(rownum)as(
select row_number () over(partition by [Col1], [Col2] order by Col3) from [table]
)
delete from cte where rownum > 1

按Col1 Col2分组删除 保留组唯一

login fail Error 18456

CSDN Blog:SQL Server Error 18456

other issues: Microsoft Docs: Troubleshooting Connect to SQL Server

查看版本

1
select @@version

作业和代理

某需求欲使用SQL Server的计划进行自动备份,启动SQL Server Agent时账户密码不正确 且该账号登录SSMS没有计划、代理等菜单

SQL Server Express没有这部分功能

sqlcmd

关于ORM

参考阮一峰《ORM 实例教程》:
面向对象编程(我们的后台)把所以实体看成对象(即Object),关系型数据库则是采用实体之间的关系(即Relation)连接数据,打通后台对象和关系数据库之间的关系而做的映射(Mapping)就是ORM。或者说ORM是通过对象实例语法,完成关系型数据库操作的技术。

举个栗子

1
2
3
SELECT id, first_name, last_name, phone, birth_date, sex
FROM persons
WHERE id = 10

编程调用SQL的写法大致是
1
2
res = db.execSql(sql);
name = res[0]["FIRST_NAME"];

ORM要实现的写法是
1
2
p = Person.get(10);
name = p.first_name;

优点:

  • 数据模型都在一个地方定义,更容易更新和维护,也利于重用代码。
  • ORM 有现成的工具,很多功能都可以自动完成,比如数据消毒、预处理、事务等等。
  • 它迫使你使用 MVC 架构,ORM 就是天然的 Model,最终使代码更清晰。
  • 基于 ORM 的业务代码比较简单,代码量少,语义性好,容易理解。
  • 你不必编写性能不佳的 SQL。

缺点

  • ORM 库不是轻量级工具,往往需要花很多精力学习和设置。
  • 对于复杂的查询,ORM 要么是无法表达,要么是性能不如原生的 SQL。
  • ORM 抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特殊的 SQL。

(删除线部分QQs不是特别赞同)

同属ORM技术的比如 Hibernate, MyBatis

关于ODBC

开放式数据库连接(Open Database Connection),连接数据库进行查询的规范(specification)

entity framwork,dbcontext, datarepository

Include, ThenInclude:

1
2
3
result = await _context.employees.Where(employee => employee.level == highlvl)
.Include(employee => employee.department)
.ThenInclude(department => department.bills).ToListAsync();

多对多关系会造成循环引用(circular reference),默认情况下序列化类会报异常,参考Add mechanism to handle circular references when serializing

Dapper.net

据说vsftp是very safe FTP, vsftp服务以ssl保护数据传输,使用22端口而不是21端口。

1
sudo apt-get install vsftpd

参数配置:/etc/vsftpd.conf,用cp命令备份下先
参数定义—>here

修改配置后重启

1
service vsftpd restart

FileZilla

FastDFS是国人大神开发的用于小文件(<500Mb)存储的分布式文件管理系统

Github: happyfish100/fastdfs

戳—>详细配置步骤,超级详细,傻瓜式,环境是Centos。对于ubuntu,安装编译环境的方式会有不同

centos编译环境

1
2
yum install git gcc gcc-c++ make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl-devel wget vi -y

ubuntu编译环境

1
apt install git gcc g++ make automake autoconf libtool libpcre3 libpcre3-dev zlib1g zlib1g-dev  libssl-dev wget vi

tracker和storage

fdfs
如配置步骤所述,fdfs安装好后有tracker配置文件和storage配置文件,前者配置tracker用于上传下载的调度,后者配置storage作为文件存储。

tracher监听storage的状态同步消息,使当client上传或下载时,提供可用的storage路径

storage可以配置为group,相同group的文件会相互拷贝(这个是需要一定时间的,在集群方案中需要考虑)

上传文件

上传
为使业务应用服务器实现上传fDFS,应实现client功能,即

  • 请求tracker 获取可用storage的 ip port等
  • 调用相应的storage接口上传文件,接受返回的file_id信息

下载文件

下载
如图,client实现下载,需

  • 请求tracker 获拉取下载的storage的 ip port等
  • 调用相应的storage接口下载文件

变量和常量

a. 用var关键字来定义变量,使用$来引用变量。

b. 寄存器变量 $0~$9,$R0~$R9

c. 系统预置变量

  • $INSTDIR
    用户定义的解压路径。
  • $PROGRAMFILES
    程序文件目录(通常为 C:\Program Files 但是运行时会检测)。
  • $COMMONFILES
    公用文件目录。这是应用程序共享组件的目录(通常为 C:\Program Files\Common Files 但是运行时会检测)。
  • $DESKTOP
    Windows 桌面目录(通常为 C:\windows\desktop 但是运行时会检测)。该常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
  • $EXEDIR
    安装程序运行时的位置。(从技术上来说你可以修改改变量,但并不是一个好方法)。
  • ${NSISDIR}
    包含 NSIS 安装目录的一个标记。在编译时会检测到。常用于在你想调用一个在 NSIS 目录下的资源时,例如:图标、界面……
  • $WINDIR
    Windows 目录(通常为 C:\windows 或 C:\winnt 但在运行时会检测)
  • $SYSDIR
    Windows 系统目录(通常为 C:\windows\system 或 C:\winnt\system32 但在运行时会检测)
  • $TEMP
    系统临时目录(通常为 C:\windows\temp 但在运行时会检测)
  • $STARTMENU
    开始菜单目录(常用于添加一个开始菜单项,使用 CreateShortCut)。该常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
  • $SMPROGRAMS
    开始菜单程序目录(当你想定位 + $STARTMENU\程序 时可以使用它)。该常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
  • $SMSTARTUP
    开始菜单程序/启动 目录。该常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
  • $QUICKLAUNCH
    在 IE4 活动桌面及以上的快速启动目录。如果快速启动不可用,仅仅返回和 + $TEMP 一样。
  • $DOCUMENTS
    文档目录。一个当前用户典型的路径形如 C:\Documents and Settings\Foo\My Documents。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
    该常量在 Windows 95 且 Internet Explorer 4 没有安装时无效。
  • $SENDTO
    该目录包含了“发送到”菜单快捷项。
  • $RECENT
    该目录包含了指向用户最近文档的快捷方式。
  • $FAVORITES
    该目录包含了指向用户网络收藏夹、文档等的快捷方式。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
    该常量在 Windows 95 且 Internet Explorer 4 没有安装时无效。
  • $MUSIC
    用户的音乐文件目录。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
    该常量仅在 Windows XP、ME 及以上才有效。
  • $PICTURES
    用户的图片目录。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
    该常量仅在 Windows 2000、XP、ME 及以上才有效。
  • $VIDEOS
    用户的视频文件目录。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
    该常量仅在 Windows XP、ME 及以上才有效。
  • $NETHOOD
    该目录包含了可能存在于我的网络位置、网上邻居文件夹的链接对象。
    该常量在 Windows 95 且 Internet Explorer 4 和活动桌面没有安装时无效。
  • $FONTS
    系统字体目录。
  • $TEMPLATES
    文档模板目录。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
  • $APPDATA
    应用程序数据目录。当前用户路径的检测需要 Internet Explorer 4 及以上。所有用户路径的检测需要 Internet Explorer 5 及以上。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
    该常量在 Windows 95 且 Internet Explorer 4 和活动桌面没有安装时无效。
  • $PRINTHOOD
    该目录包含了可能存在于打印机文件夹的链接对象。
    该常量在 Windows 95 和 Windows 98 上无效。
  • $INTERNET_CACHE
    Internet Explorer 的临时文件目录。
    该常量在 Windows 95 和 Windows NT 且 Internet Explorer 4 和活动桌面没有安装时无效。
  • $COOKIES
    Internet Explorer 的 Cookies 目录。
    该常量在 Windows 95 和 Windows NT 且 Internet Explorer 4 和活动桌面没有安装时无效。
  • $HISTORY
    Internet Explorer 的历史记录目录。
    该常量在 Windows 95 和 Windows NT 且 Internet Explorer 4 和活动桌面没有安装时无效。
  • $PROFILE
    用户的个人配置目录。一个典型的路径如 C:\Documents and Settings\Foo。
    该常量在 Windows 2000 及以上有效。
  • $ADMINTOOLS
    一个保存管理工具的目录。这个常量的内容(所有用户或当前用户)取决于 SetShellVarContext 设置。默认为当前用户。
    该常量在 Windows 2000、ME 及以上有效。
  • $RESOURCES
    该资源目录保存了主题和其他 Windows 资源(通常为 C:\Windows\Resources 但在运行时会检测)。
    该常量在 Windows XP 及以上有效。
  • $RESOURCES_LOCALIZED
    该本地的资源目录保存了主题和其他 Windows 资源(通常为 C:\Windows\Resources\1033 但在运行时会检测)。
    该常量在 Windows XP 及以上有效。
  • $CDBURN_AREA
    一个在烧录 CD 时储存文件的目录。.
    该常量在 Windows XP 及以上有效。
  • $HWNDPARENT
    父窗口的十进制 HWND。
  • $PLUGINSDIR
    该路径是一个临时目录,当第一次使用一个插件或一个调用 InitPluginsDir 时被创建。该文件夹当解压包退出时会被自动删除。这个文件夹的用意是用来保存给 InstallOptions 使用的 INI 文件、启动画面位图或其他插件运行需要的文件。

函数

NSIS函数本身没有输出输出,但是可以访问变量和堆栈

使用Pop Push实现输入输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Function LCIDtoTAG 
${case} "1036"
Push "fr_FR"
${break}
${case} "1049"
Push "ru_RU"
${break}
${case} "1041"
Push "ja_JP"
${break}
${case} "2052"
Push "zh_CN"
${break}
${default}
Push "en_US"
${break}
${endswitch}
FunctionEnd
Section
Call LCIDtoTAG
Pop $0
SectionEnd

跳转

1
2
3
4
StrCmp $1 "" +1 +2
DetailPrint "parameter is empty"

DetailPrint "parameter is not empty"

+n是从该语句向下偏移的“指针”,空行过滤掉不计入偏移量

—> NSIS收录插件 https://nsis.sourceforge.io/Category:Plugins

—> 插件本体是托管dll文件,区分unicode编码和ansi编码

—> NSIS默认检索在NSIS代码目录下的Plugins文件夹,作为插件路径,或可使用标识符!addplugindir 指定其他目录

一个栗子

1
2
3
4
5
6
7
8
9
!ifndef TARGETDIR
!ifdef NSIS_UNICODE
!define TARGETDIR "..\binU"
!else
!define TARGETDIR "..\bin"
!endif
!endif

!addplugindir "${TARGETDIR}"

曾将!ifdef误解为if not define,NSIS用!xxx做关键字真的是坑。

NGX-Translate is an internationalization library for Angular. NGX-Translate is also extremely modular. It is written in a way that makes it really easy to replace any part with a custom implementation in case the existing one doesn’t fit your needs.

Why ngx-translate exists if we already have built-in Angular i18n

关于载入翻译文件

集成步骤

1
npm install @ngx-translate/core @ngx-translate/http-loader --save

在模块中引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {TranslateModule} from '@ngx-translate/core';

export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient]
},
defaultLanguage: 'en'
})
],
bootstrap: [AppComponent]
})
export class AppModule { }

组件注入及初始化
1
2
3
4
5
6
7
8
export class AppComponent {
constructor(translate: TranslateService) {
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en');
// the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use('en');
}
}

在模板中使用管道标记待翻译标记
1
2
3
4
5
<p>{{"sayHi" | translate}}</p>

<p>{{'sayHiWithParameter' | translate:userObj}}</p>

<p>{{ 'ROLES.' + role | uppercase | translate }}</p>

翻译文件,对于HTTP Loader是以Locale_ID命名的Json
1
2
3
4
5
6
src
├───assets
│ ├───i18n
│ │ en.json
│ │ fr.json
│ │ zh.json

翻译格式
1
2
3
4
5
6
7
8
{
"introductionHeader":"你好",
"ROLE":{
"ADMIN": "管理员",
"USER": "用户"
},
"sayHiWithParameter":"你好,{{pride}}的{{lastname}}}先生"
}

在ts代码中引用翻译
1
2
3
4
translate.get('HELLO', {value: 'world'}).subscribe((res: string) => {
console.log(res);
//=> 'hello world'
});

使用 PO Loader

官方提供给的 @biesbjerg/ngx-translate-po-http-loader,很遗憾,并不好使,或许是因为新版本的gettext-parser将msgctxt(信息上下文)作为一级父节点,而ngx-translate-po-http-loader一直没有更新使支持msgctxt

部分zh_CN.po文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Language: zh_CN\n"
"X-Qt-Contexts: true\n"

#: ../scanflow/res/qml/Dialog/AboutDialog.qml:25
msgctxt "AboutDialog|"
msgid "About"
msgstr "关于"

#: ../scanflow/res/qml/Dialog/AboutDialog.qml:52
msgctxt "AboutDialog|"
msgid "Product Version"
msgstr "产品版本"

通过上文“翻译格式”可知,ngx-translate允许多级属性mapping,如
1
<h1>{{ "AboutDialog|.About" |translate}}</h1>

另外,gettext-parser会将分段的msgid和msgstr进行合并,即
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
msgctxt "ErrorMessage|"
msgid ""
"Application preferences have been damaged. Reinstall the application to "
"solve the problem."
msgstr ""
"Předvolby aplikace byly poškozeny. Problém vyřešíte opětovnou instalací "
"aplikace."
====>
{
...
"ErrorMessage|":{
msgctxt:"ErrorMessage|",
msgid:"Application preferences have been damaged. Reinstall the application to solve the problem.",
msgstr:["Předvolby aplikace byly poškozeny. Problém vyřešíte opětovnou instalací aplikace."]
}
}

原理见@ngx-translate/core/fesm2015/ngx-translate-core.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
getValue(target, key) {
/** @type {?} */
let keys = typeof key === 'string' ? key.split('.') : [key];
key = '';
do {
key += keys.shift();
if (isDefined(target) && isDefined(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
target = target[key];
key = '';
}
else if (!keys.length) {
target = undefined;
}
else {
key += '.';
}
} while (keys.length);
return target;
}

结合ngx-translate解析翻译时的逻辑,在实现PO Loader的getTranslation方法(关于自定义loader的继承和实现,ngx-translate有指引 -> link)时,应将msgctxt,msgid转为父子属性形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public getTranslation(lang: string): Observable<any> {
return this._http
.get(`${this._prefix}/${lang}${this._suffix}`, { responseType: 'text' })
.pipe(
map((contents: string) => this.parse(contents)));
}

/**
* Parse po file
* @param contents
* @returns {any}
*/
public parse(contents: string): any {
let translations: { [key: string]: object | string } = {};

const po = gettext.po.parse(contents, 'utf-8');
if (!po.translations.hasOwnProperty(this.domain)) {
return translations;
}

Object.keys(po.translations)
.forEach(domain => {
if (domain.length === 0) { return; }
if (po.translations[domain].msgstr) { // there is no msgctxt
translations[domain] = po.translations[domain].msgstr;
} else {
// context
translations[domain] = {};
Object.keys(po.translations[domain]).forEach(key => {
let translation: string | Array<string> = po.translations[domain][key].msgstr.pop();
if (translation instanceof Array && translation.length > 0) {
translation = translation[0];
}
if (key.length > 0 && translation.length > 0) {
translations[domain][key] = translation;
}
});
}
});
return translations;
}

引入自己的 loader module
1
2
3
import { TranslatePoHttpLoader } from './edited-po-loader';
export function createTranslateLoader(http: HttpClient) {
return new TranslatePoHttpLoader(http, 'assets/i18n', '.po');

缺失的翻译 handle missing translations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core';

export class MyMissingTranslationHandler implements MissingTranslationHandler {
handle(params: MissingTranslationHandlerParams) {
return 'some value';
}
}
@NgModule({
imports: [
BrowserModule,
TranslateModule.forRoot({
missingTranslationHandler: {provide: MissingTranslationHandler, useClass: MyMissingTranslationHandler},
useDefaultLang: false
})
],
providers: [

],
bootstrap: [AppComponent]
})
export class AppModule { }

plural and select

这个库没有相应的api与i18n的 plural / select 模式相对应,对于这些场景需要使用ngIf或ngSwitch指令实现

ng build, ng serve是JIT, ng build —aot, ng build —prod, ng serve —aot是AOT 从Angular 9开始,默认情况下,对于提前编译器,编译选项设置为true。

JIT(Just in Time)由浏览器将源码编译成js执行,QQs:浏览器居然可以编译代码!
AOT(Ahead of Time)先编译成可执行的js,再交给浏览器

The Angular ahead-of-time (AOT) compiler converts Angular HTML and TypeScript code into efficient JavaScript code during the build phase, before the browser downloads and runs that code. This is the best compilation mode for production environments, with decreased load time and increased performance compared to just-in-time (JIT) compilation.AOT 编译器在浏览器下载并运行之前,将Angular HTML、 ts代码转为es5代码,是生产环境的最佳实践,相比JIT更能缩短加载时间并提高性能
(aot会根据angular.json 的配置生成到/dist之类的目录) 在angular.json中配置build命令的选项,包括生成目录等

what is aot and jit compiler in angular

Li Mei’s Blog: Angular深入理解编译机制
采取两种编译方式,注意修改angular.json architect.build.option.aot改为false,比较不使用—aot和使用时生成js的内容(见main.js, vendor.js是包含compiler等工具链的源码),可见生成的js包含angular模板语法,只是ts编译成了es5

另,使用source-map-explorer工具分析编译生成的js文件

1
npx source-map-explorer dist/main.js --no-border-checks

Angular Doc: AOT工作原理
YouTube: ng-conf
Angular编译机制(AOT、JIT)
AOT和ngc
ngc是专用于Angular项目的tsc替代者。它内部封装了tsc,还额外增加了用于Angular的选项、输出额外的文件。配置见于tsconfig.json, tsc读取tsconfig配置文件的compilerOptions部分,ngc读取angularCompilerOptions部分。

to be continue

数据层应用程序 (DAC) 是一个逻辑数据库管理实体,用于定义与用户数据库关联的所有 SQL Server 对象,如表、视图和实例对象(包括登录名)。 DAC 是 SQL Server 数据库部署的一个自包含单元,它使数据层开发人员和数据库管理员能够将 SQL Server 对象打包到一个名为“DAC 包”(也称作 DACPAC)的可移植项目中。