EntityFramework
参考
深入研究EF Core AddDbContext 引起的内存泄露的原因
EF6
ORM
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。 这也同时暗示着额外的执行开销;然而,如果ORM作为一种中间件实现,则会有很多机会做优化,而这些在手写的持久层并不存在。 更重要的是用于控制转换的元数据需要提供和管理;但是同样,这些花费要比维护手写的方案要少;而且就算是遵守ODMG规范的对象数据库依然需要类级别的元数据。
数据库提供程序
Sqlserver
引入的包
- Microsoft.EntityFrameworkCore.SqlServer
appsettings.json中链接字符串的配置
- "CoreMVCContext": "Server=.;Database=jlhrtest;Persist Security Info=True;User ID=sa;Password=1"
Mysql
参考:
- .net6使用mysql参考:从零开始手把手教你,.net 6用EF Core基本创建表,迁移到mysql数据库
引入的包
- MySql.Data.EntityFrameworkCore
appsettings.json中链接字符串的配置
- "CoreMVCContext": "server=localhost;port=3306;database=JLHRtest;uid=root;pwd=root;CharSet=utf8"
EF用到的NuGet包常用命令
dotnet ef与NuGet包的【程序包管理器控制台】
这里列出的都是NuGet包的【程序包管理器控制台】的命令,如果是用dotnet ef命令,要在项目的文件夹下打开CMD执行
全部安装ef命令工具:dotnet tool install --global dotnet-ef
添加迁移:dotnet ef migrations add 迁移名称
更新数据库:dotnet ef database update
迁移注意事项
- 迁移前检查上下文ConText文件,如果没有DbSet类可以不用迁移,下面红色框内是需要迁移的DbSet类
- Migrations文件夹下如果没有迁移文件,先添加迁移,后更新数据库。
- 迁移文件过多的,可以删除Migrations文件夹,重新添加迁移,重点注意:在生产的环境禁用此操作
- 迁移是提示有多个上下文Context
- 要在迁移命令后用-Context + 上下文t名称,添加迁移和更新数据库时都需要在后面增加,例如:Add-Migration user2020-11-24 -Context UserContext
- 迁移时提示引用报错
- 一般迁移的时候,会启动Startup.cs文件,如果这个文件引用了其他项目或者服务、或者使用了注册中心、心跳检测等,迁移的时候也会调用的,所以可以把数据库以外的配置和依赖注入都注释掉
- 一般迁移的时候,会启动Startup.cs文件,如果这个文件引用了其他项目或者服务、或者使用了注册中心、心跳检测等,迁移的时候也会调用的,所以可以把数据库以外的配置和依赖注入都注释掉
- 两个地方都要锁定同一个项目问题,否则失败
-
创建迁移且更新到数据库
备注:每次数据库实体类有变更后比需要执行这两条的命令
- 添加迁移命令:Add-Migration 迁移名称
- 名称可任意定义但不能重复,建议使用业务+日期+时间来命名,避免重复,例如user20200704-1911,或者不要业务,直接日期+时间
- 更新到数据库命令:Update-Database
- 此操作会根据全部的迁移文件去数据库中生成对应的数据库、表、字段
- 执行更新命令后,如果手工去删除数据库的一些表或字段,再重新执行更新也没有用的,不会把删除的表或字段给添加回来的,除非把数据库也删除了才会整个数据库添加回来
多个上下文多个数据库
多个上下文
多个连接字符串
多个上下文服务注册
迁移时需要锁定上下文
添加迁移和更新数据库操作时要在命令后加-Context + 上下文名称 , 来锁定上下文。
vs2019创建ASP.NET Core MVC新项目自带有EFCore、上下文Context、控制器、视图
创建asp.net core mvc项目,不进行身份证验证
刚建立好的项目是没有NuGet包的
创建数据库实体模型
鼠标右键Controllers=>添加=》控制器
选择【视图使用Entity Framework的MVC控制器】
选择建立好的数据库实体模型、点击加号+创建Context上下文、上下可改名为“CoreMVCContext”,点击添加,再点击添加
如果生成报错,那就清理解决方案,然后生成解决方案,再重新生成试试
生成成功的结果如下:自动创建了4个包、上下文类 、控制器、视图
自动生成了上下文类
/// <summary> /// 数据库上下文类 /// </summary> public class CoreMVCContext : DbContext { public CoreMVCContext (DbContextOptions<CoreMVCContext> options) : base(options) { } public DbSet<CoreMVC.Models.Zldept> Zldept { get; set; } }
appsettings.json配置文件自动添加了CoreMVCContext节点,需要修改下数据库连接地址和密码
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "CoreMVCContext": "Server=.;Database=jlhrtest;Persist Security Info=True;User ID=sa;Password=1" } }
Startup.cs文件也自动注册上下文服务,且加载数据连接信息,默认是SqlServer,可更改为他数据库,例如MySql的要把UseSqlServer改为UseMySQL
public void ConfigureServices(IServiceCollection services) { //注册上下文服务且加载appsettings.json配置文件中“CoreMVCContext”节点的信息 //默认是SqlServer数据库,如果用MySql数据库,要把UseSqlServer改为UseMySQL services.AddDbContext<CoreMVCContext>(options => options.UseSqlServer(Configuration.GetConnectionString("CoreMVCContext"))); }
创建模型
键
- 约定是ID或<表名ID>为主键,也可以通过[
索引
- 不能使用数据批注创建索引,要在数据库上下文类中重写OnModelCreating方法
- 索引列:HasIndex
- 索引唯一性:IsUnique
protected override void OnModelCreating(ModelBuilder modelBuilder) { //索引设置 modelBuilder.Entity<Person>() .HasIndex(p => new { p.FirstName, p.LastName }) //索引列 .IsUnique(); //索引唯一性 }
关系
主键
外键
导航属性
查询数据
常用查询方法
- ToList:集合查询
- Single、singleordefault:查询单个实体
- Where:过滤
分页
EFCore3.0以上的分页是使用Skip().Take(),因为3.0以上不再支持sql2008,统一使用sql2012新增的Offset Fetch分页
如果要兼容sql2008的Row_Number分页要用EFCore2.1的UseRowNumberForPaging()
查询几个字段
//查询一个字段 var a = _context.Zlemployee.Select(e => e.Name).ToList(); //查询2个字段 var b = _context.Zlemployee.Select(e=>new { e.Code,e.Name}).ToList();
复杂查询运算符
参考
数据库与EF转换
- in、not in:Contains()
- inner join: Join()
- left join: DefaultIfEmpty()
- GroupJoin: 分组左连接
- is null: string.IsNullOrEmpty()
- distinct: Select(x=>new { } ).Distinct()
- group by: GroupBy().Select(x=>x.Key)
join:连接
模型代码:
/// <summary> /// 部门表数据库模型 /// </summary> public class Zldept { public int ID { get; set; } public string Code { get; set; }//部门编码 public string Name { get; set; }//部门名称 } /// <summary> /// 人事档案表数据库 /// </summary> public class Zlemployee { public int ID { get; set; } public string Code { get; set; }//工号 public string Name { get; set; }//姓名 public string ZleptCode { get; set; }//部门编码 }
控制器代码:
// left join :返回第一个表(左表)的全部记录,第二个表(右表)没有匹配的数据也会显示 // inner join :返回两个表交集的记录,要两个表同时有数据匹配才会显示 // 总结:因为inner join比left join性能高,如果业务允许尽量推荐使用inner join,但是有不少业务是必须使用left join //LINQ:inner join: 用join on equals var result = (from e in _context.Zlemployee join d in _context.Zldept on e.ZleptCode equals d.Code select new ZlemployeeView { ID = e.ID, Code = e.Code, Name = e.Name, ZldeptName = d.Name, E_zhiwuName = z.Name } ).ToList(); //LINQ:inner join: 用from var result = (from e in _context.Set<Zlemployee>() from d in _context.Set<Zldept>().Where(d => d.Code == e.ZleptCode) select new { ID = e.ID, Code = e.Code, Name = e.Name, ZldeptName = d.Name } ).ToList(); //Lambda:inner join:用Join var result = _context.Zlemployee .Join(_context.Zldept, a => a.ZleptCode, b => b.Code, (a, b) => new { a.Code, a.Name, ZldeptName = b.Name, a.E_zhiwuID }) .Join(_context.E_zhiwu, c => c.E_zhiwuID, d => d.ID, (c, d) => new { c.Code, c.Name, ZldeptName = c.ZldeptName, E_zhiwuName = d.Name }) .ToList(); //LINQ:Left Join: 用into、DefaultIfEmpty() var result = (from e in _context.Zlemployee join d in _context.Zldept on e.ZleptCode equals d.Code into zlemptgrouping from d in zlemptgrouping.DefaultIfEmpty() select new { Code = e.Code, Name = e.Name, ZldeptName = d.Name } ).ToList();
生成的sql语句:
inner join:
SELECT [z].[Code], [z].[Name], [z0].[Name] AS [ZldeptName], [e].[Name] AS [E_zhiwuName] FROM [Zlemployee] AS [z] INNER JOIN [Zldept] AS [z0] ON [z].[ZleptCode] = [z0].[Code] INNER JOIN [E_zhiwu] AS [e] ON [z].[E_zhiwuID] = [e].[ID]
left join:
SELECT [z].[Code], [z].[Name], [z0].[Name] AS [ZldeptName], [e].[Name] AS [E_zhiwuName] FROM [Zlemployee] AS [z] LEFT JOIN [Zldept] AS [z0] ON [z].[ZleptCode] = [z0].[Code] LEFT JOIN [E_zhiwu] AS [e] ON [z].[E_zhiwuID] = [e].[ID]
DISTINCT:过滤重复
var test = db.Set<Student>().Select(x => new { x.Name, x.Dept }).Distinct();
生成SQL语句
SELECT [Distinct1].[C1] AS [C1], [Distinct1].[Name] AS [Name], [Distinct1].[Dept] AS [Dept] FROM ( SELECT DISTINCT [Extent1].[Name] AS [Name], [Extent1].[Dept] AS [Dept], 1 AS [C1] FROM [dbo].[Students] AS [Extent1] ) AS [Distinct1]
group by:分组
var test = db.Students.GroupBy(s=>new { s.Name, s.Dept }).Select(s=> new { s.Key, counts = s.Count() }).ToList();
加载相关数据
预先加载
加载关联其他实体的数据,可以加载单个实体,也可以集合
Include()
ThenInclude()
显式加载
Entry()
延迟加载
默认关闭,需要手动开启
增加、修改、删除
参考:保存数据
批量更新、删除
参考:Entity Framework Core 5中实现批量更新、删除
迁移
应用迁移
- SQL 脚本:生产环境首次创建数据库是使用,在项目中使用命令生产SQL脚本: Script-Migration,也可在开发环境的数据库中生成整个数据库脚本。
- 幂等 SQL 脚本:生产环境持续迁移时使用,在项目中使用命令生产溟等SQL脚本:Script-Migration -Idempotent
- 就是生成脚本前检查是否迁移过,迁移过的就不在生成脚本,只生成没迁移过的脚本,参考:冥等
- 在运行时应用迁移:在程序中使用编程方式迁移
反向工程(基架)
参考:
执行sql语句
FromSqlRaw
基于原始SQL查询创建LINQ查询,代替旧版的FromSql、SqlQuery
基本原生 SQL 查询
可使用 FromSqlRaw
扩展方法基于原始 SQL 查询开始 LINQ 查询。 FromSqlRaw
只能在直接位于 DbSet<>
上的查询根上使用
var blogs = context.Blogs .FromSqlRaw("SELECT * FROM dbo.Blogs") .ToList();
执行存储过程
var blogs = context.Blogs .FromSqlRaw("EXECUTE dbo.GetMostPopularBlogs") .ToList();
传递参数
//在 SQL 查询字符串中包含形参占位符并提供额外的实参,将单个形参传递到存储过程 Blogs是Context上下文中配置的实体类属性 var user = "johndoe"; var blogs = context.Blogs.FromSqlRaw("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user).ToList(); //使用字符串内插语法,该值会转换为 DbParameter,且不易受到 SQL 注入攻击 var user = "johndoe"; var blogs = context.Blogs.FromSqlInterpolated($"EXECUTE dbo.GetMostPopularBlogsForUser {user}").ToList(); //DbParameter 并将其作为参数值提供。 由于使用了常规 SQL 参数占位符而不是字符串占位符,因此可安全地使用 FromSqlRaw var user = new SqlParameter("user", "johndoe"); var blogs = context.Blogs.FromSqlRaw("EXECUTE dbo.GetMostPopularBlogsForUser @user", user).ToList(); //借助 FromSqlRaw,可以在 SQL 查询字符串中使用已命名的参数,这在存储的流程具有可选参数时非常有用 var user = new SqlParameter("user", "johndoe"); var blogs = context.Blogs.FromSqlRaw("EXECUTE dbo.GetMostPopularBlogsForUser @filterByUser=@user", user).ToList();
Database.ExecuteSqlRaw
针对数据库执行给定的SQL,并返回受影响的行数,代替旧版的ExecuteSqlCommand
日志记录、时间和诊断
查看EF生成的SQL语句
参考
.NET Core实用技巧(一)如何将EF Core生成的SQL语句显示在控制台中
简单的日志记录--NET5后建议使用
在数据库上下文类中增加下面代码
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .LogTo(info => { Console.WriteLine(info.Contains("Executed") ? info : null);//输出SQL执行脚本,需要控制台运行项目 });
连接池:DbContextPool
参考
官方说明:DbContextPool 连接池 、 AddDbContextPool API说明 、SQL Server 连接池 (ADO.NET)
EF Core 小坑:DbContextPool 会引起数据库连接池连接耗尽
博客园升级到.net core3.0翻车之:峰回路转:去掉 DbContextPool 后 Windows 上的 .NET Core 版博客表现出色
概念
在版本.net core 2.0 中,我们引入了一种在依赖关系注入中注册自定义 DbContext 类型的新方法,即以透明形式引入可重用 DbContext 实例的池。 要使用 DbContext 池,请在服务注册期间使用 AddDbContextPool
而不是 AddDbContext
:services.AddDbContextPool<BloggingContext>( options => options.UseSqlServer(connectionString));
如果使用此方法,那么在控制器请求 DbContext 实例时,我们会首先检查池中有无可用的实例。 请求处理完成后,实例的任何状态都将被重置,并且实例本身会返回池中。
从概念上讲,此方法类似于连接池在 ADO.NET 提供程序中的运行原理,并具有节约 DbContext 实例初始化成本的优势。
源码阅读
此类中提示:这是一个内部API,支持Entity Framework Core基础结构,并且不受与公共API相同的兼容性标准的约束。·在任何版本中,它都可能更改或删除,恕不另行通知。您仅应非常谨慎地在代码中直接使用它,并且知道这样做会导致在更新到新的Entity Framework Core版本时导致应用程序失败。
DbContextPool继承IDbContextPool接口
DbContextPool构造函数判断是都第一次使用,如果是就通过激活器CreateActivator来创建
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Internal { /// <summary> /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. ///翻译: /// 这是一个内部API,支持Entity Framework Core基础结构,并且不受与公共API相同的兼容性标准的约束。 在任何版本中,它都可能更改或删除,恕不另行通知。 /// 您仅应非常谨慎地在代码中直接使用它,并且知道这样做会导致在更新到新的Entity Framework Core版本时导致应用程序失败。 /// ///DbContextPool类源码阅读步骤: /// 1 查看构造函数 /// 2 DbContextPool类被EntityFrameworkServiceCollectionExtensions类的两个AddDbContextPool方法分别调用了,相当于调用2次 /// ///疑问:ef默认的AddDbContext有没有使用连接池的?难道一定要一定要AddDbContextPool才会使用连接池 /// </summary> public class DbContextPool<TContext> : IDbContextPool<TContext>, IDisposable, IAsyncDisposable where TContext : DbContext { private const int DefaultPoolSize = 32; //ConcurrentQueue 并发队列:表示线程安全的先进先出(FIFO)集合。 private readonly ConcurrentQueue<IDbContextPoolable> _pool = new ConcurrentQueue<IDbContextPoolable>(); private readonly Func<DbContext> _activator; private int _maxSize; private int _count; /// <summary> /// 构造函数 /// </summary> /* 使用方式:services.AddDbContextPool<BloggingContext>(options => options.UseSqlServer(connectionString)) * DbContextOptions:上下文选项类 * FindExtension:获取指定类型的扩展名。 如果未配置指定类型的扩展名,则返回null */ public DbContextPool([NotNull] DbContextOptions<TContext> options) { //最大值 _maxSize = options.FindExtension<CoreOptionsExtension>()?.MaxPoolSize ?? DefaultPoolSize; //冻结:指定不应再配置此选项对象 options.Freeze(); //创建激活器:参数是上下文配置类 ------重点 _activator = CreateActivator(options); if (_activator == null) { //InvalidOperationException:初始化的新实例System.InvalidOperationException使用指定的错误消息初始化。 throw new InvalidOperationException( CoreStrings.PoolingContextCtorError(typeof(TContext).ShortDisplayName())); } } /// <summary> /// 创建激活器:参数是上下文配置类 /// </summary> private static Func<DbContext> CreateActivator(DbContextOptions<TContext> options) { //DeclaredConstructors:获取当前类型声明的构造函数的集合 var constructors = typeof(TContext).GetTypeInfo().DeclaredConstructors .Where(c => !c.IsStatic && c.IsPublic) .ToArray(); if (constructors.Length == 1) { var parameters = constructors[0].GetParameters(); if (parameters.Length == 1 && (parameters[0].ParameterType == typeof(DbContextOptions) || parameters[0].ParameterType == typeof(DbContextOptions<TContext>))) { //Expression:构造一个新的System.Linq.Expressions.Expression实例 //Lambda:创建一个System.Linq.Expressions.Expression`1,其中在编译时知道委托类型,并带有参数表达式数组。 //Compile:将表达式树描述的lambda表达式编译为可执行代码,并生成代表lambda表达式的委托。 return Expression.Lambda<Func<TContext>>(Expression.New(constructors[0], Expression.Constant(options))).Compile(); } } return null; } /// <summary> /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// </summary> public virtual IDbContextPoolable Rent() { if (_pool.TryDequeue(out var context)) { Interlocked.Decrement(ref _count); Check.DebugAssert(_count >= 0, $"_count is {_count}"); return context; } context = _activator(); context.SnapshotConfiguration(); return context; } /// <summary> /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// </summary> public virtual void Return(IDbContextPoolable context) { if (Interlocked.Increment(ref _count) <= _maxSize) { context.ResetState(); _pool.Enqueue(context); } else { PooledReturn(context); } } /// <summary> /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// </summary> public virtual async ValueTask ReturnAsync(IDbContextPoolable context, CancellationToken cancellationToken = default) { if (Interlocked.Increment(ref _count) <= _maxSize) { await context.ResetStateAsync(cancellationToken).ConfigureAwait(false); _pool.Enqueue(context); } else { PooledReturn(context); } } private void PooledReturn(IDbContextPoolable context) { Interlocked.Decrement(ref _count); Check.DebugAssert(_maxSize == 0 || _pool.Count <= _maxSize, $"_maxSize is {_maxSize}"); context.ClearLease(); context.Dispose(); } /// <summary> /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// </summary> public virtual void Dispose() { _maxSize = 0; while (_pool.TryDequeue(out var context)) { context.ClearLease(); context.Dispose(); } } /// <summary> /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// </summary> public virtual async ValueTask DisposeAsync() { _maxSize = 0; while (_pool.TryDequeue(out var context)) { context.ClearLease(); await context.DisposeAsync().ConfigureAwait(false); } } } }
DbContextPool类被EntityFrameworkServiceCollectionExtensions类的AddDbContextPool方法用到
public static IServiceCollection AddDbContextPool<TContextService, TContextImplementation>( [NotNull] this IServiceCollection serviceCollection, [NotNull] Action<IServiceProvider, DbContextOptionsBuilder> optionsAction, int poolSize = 128) where TContextImplementation : DbContext, TContextService where TContextService : class { Check.NotNull(serviceCollection, nameof(serviceCollection)); Check.NotNull(optionsAction, nameof(optionsAction)); AddPoolingOptions<TContextImplementation>(serviceCollection, optionsAction, poolSize); serviceCollection.TryAddSingleton<IDbContextPool<TContextImplementation>, DbContextPool<TContextImplementation>>(); serviceCollection.AddScoped<IScopedDbContextLease<TContextImplementation>, ScopedDbContextLease<TContextImplementation>>(); serviceCollection.AddScoped<TContextService>( sp => sp.GetRequiredService<IScopedDbContextLease<TContextImplementation>>().Context); return serviceCollection; } public static IServiceCollection AddPooledDbContextFactory<TContext>( [NotNull] this IServiceCollection serviceCollection, [NotNull] Action<IServiceProvider, DbContextOptionsBuilder> optionsAction, int poolSize = 128) where TContext : DbContext { Check.NotNull(serviceCollection, nameof(serviceCollection)); Check.NotNull(optionsAction, nameof(optionsAction)); AddPoolingOptions<TContext>(serviceCollection, optionsAction, poolSize); /* 如果尚未注册,则将TImplementation中指定的指定TService作为Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton * 服务实现类型添加到集合中。 */ serviceCollection.TryAddSingleton<IDbContextPool<TContext>, DbContextPool<TContext>>(); serviceCollection.TryAddSingleton<IDbContextFactory<TContext>, PooledDbContextFactory<TContext>>(); return serviceCollection; }
DbContextPool类的方法被DbContextLease结构用到
public DbContextLease([NotNull] IDbContextPool contextPool, bool standalone) { _contextPool = contextPool; _standalone = standalone; var context = _contextPool.Rent(); Context = context; context.SetLease(this); } public void Release() { if (Release(out var pool, out var context)) { pool.Return(context); } } public ValueTask ReleaseAsync() => Release(out var pool, out var context) ? pool.ReturnAsync(context) : new ValueTask(); private bool Release(out IDbContextPool pool, out IDbContextPoolable context) { pool = _contextPool; context = Context; _contextPool = null; Context = null; return pool != null; }
ToList
ef 三表join,三表left join
GroupBy
本文版权归作者和博客园共有,欢迎转载,但必须在文章页面给出原文链接,否则保留追究法律责任的权利。