EF Core CRUD

上篇文章中,我们已经基本入门了EFCore,搭建了一个简单的EFCore项目,本文开始简单使用下EF,做增删改查的相关操作;

一、数据新增操作(C)

        public static async void Insert_测试新增数据1()
        {
            var myDbContext = new MyDbContext();
            if (myDbContext.TestTables.Any(p => p.Id == 1)) return;
            var newEntity = new TestTable
            {
                Id = 1,
                Name = "主表数据1"
            };

            await myDbContext.TestTables.AddAsync(newEntity);
            myDbContext.SaveChanges();

            Console.WriteLine($"TestTable Insert Success");
            Console.WriteLine($"------------------------");
        }

是不是很简单的代码?so eays ...
我们还是来分析下整体代码的运行;
1.首先我们自己在代码段中新增了一个实体对象,并对其字段做赋值,然后通过Add方法加入到DbSet中
2.通过DbContext.SaveChanges提交数据到数据库保存

那EF是如何偷偷的在背后帮我们完成这一切的呢?

EFCore实体四状态, 如下四种状态;

 public enum EntityState
    {
        /// <summary>
        ///     The entity is not being tracked by the context.
        /// </summary>
        Detached = 0,
        /// <summary>
        ///     The entity is being tracked by the context and exists in the database. Its property
        ///     values have not changed from the values in the database.
        /// </summary>
        Unchanged = 1,
        /// <summary>
        ///     The entity is being tracked by the context and exists in the database. It has been marked
        ///     for deletion from the database.
        /// </summary>
        Deleted = 2,
        /// <summary>
        ///     The entity is being tracked by the context and exists in the database. Some or all of its
        ///     property values have been modified.
        /// </summary>
        Modified = 3,
        /// <summary>
        ///     The entity is being tracked by the context but does not yet exist in the database.
        /// </summary>
        Added = 4
    }

Detached : 实体未被跟踪
Unchanged:未修改
Deleted : 删除状态
Modified:修改状态
Added:新增状态
Detached 未被跟踪状态,很多同学可能无法理解了,EFCore会默认自动跟踪实体信息,用来维护实体状态,也是方便后续提交时的处理;EFCore提供两种查询方法,跟踪查/非跟踪查,跟踪查得到的数据是Unchanged,而非跟踪查的到的数据是Detached,这两种方式我们后面详细说明,这里先简单描述下;

EFCore管理内存实体
查看DbContext源码中的Add方法,跟踪方法,发现Add方法会调用到 EntityReferenceMap.cs 类中的Update方法 (下面的源码内容),此方法中EFCore会在内存中维护我们操作的实体信息,将我们操作的实体信息管理到内存中(我们的增删改查操作,EFCore都会再内存维护,方法中只是对实体状态维护,SaveChanges才会提交);

 public virtual void Update(
            [NotNull] InternalEntityEntry entry,
            EntityState state,
            EntityState? oldState)
        {
            var mapKey = entry.Entity ?? entry;
            var entityType = entry.EntityType;
            if (_hasSubMap && entityType.HasDefiningNavigation())
            {
                if (_dependentTypeReferenceMap == null)
                {
                    _dependentTypeReferenceMap = new Dictionary<IEntityType, EntityReferenceMap>();
                }
                if (!_dependentTypeReferenceMap.TryGetValue(entityType, out var dependentMap))
                {
                    dependentMap = new EntityReferenceMap(hasSubMap: false);
                    _dependentTypeReferenceMap[entityType] = dependentMap;
                }
                dependentMap.Update(entry, state, oldState);
            }
            else
            {
                if (oldState.HasValue)
                {
                    Remove(mapKey, entityType, oldState.Value);
                }
                if (!oldState.HasValue || state != EntityState.Detached)
                {
                    switch (state)
                    {
                        case EntityState.Detached:
                            if (_detachedReferenceMap == null)
                            {
                                _detachedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
                            }
                            _detachedReferenceMap[mapKey] = entry;
                            break;
                        case EntityState.Unchanged:
                            if (_unchangedReferenceMap == null)
                            {
                                _unchangedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
                            }
                            _unchangedReferenceMap[mapKey] = entry;
                            break;
                        case EntityState.Deleted:
                            if (_deletedReferenceMap == null)
                            {
                                _deletedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
                            }
                            _deletedReferenceMap[mapKey] = entry;
                            break;
                        case EntityState.Modified:
                            if (_modifiedReferenceMap == null)
                            {
                                _modifiedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
                            }
                            _modifiedReferenceMap[mapKey] = entry;
                            break;
                        case EntityState.Added:
                            if (_addedReferenceMap == null)
                            {
                                _addedReferenceMap = new Dictionary<object, InternalEntityEntry>(ReferenceEqualityComparer.Instance);
                            }
                            _addedReferenceMap[mapKey] = entry;
                            break;
                    }
                }
            }
        }

代码中就是针对不同状态的实体,EF采用不同的集合进行维护,按照我们的测试代码,会将新增实体放入上面的_addedReferenceMap 集合中,方便EFCore做提交时的操作;
EFCore SaveChanges
然后来看下将实体加入到内存集合后,提交操作到底做了什么
首先跟踪到 DbContext的 SaveChanges方法,其内部会调用StateManger.SaveChanges方法,代码如下:

        public virtual int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            if (ChangedCount == 0)
            {
                return 0;
            }
            var entriesToSave = GetEntriesToSave(cascadeChanges: true);
            if (entriesToSave.Count == 0)
            {
                return 0;
            }
            try
            {
                var result = SaveChanges(entriesToSave);
                if (acceptAllChangesOnSuccess)
                {
                    AcceptAllChanges((IReadOnlyList<IUpdateEntry>)entriesToSave);
                }
                return result;
            }
            catch
            {
                foreach (var entry in entriesToSave)
                {
                    ((InternalEntityEntry)entry).DiscardStoreGeneratedValues();
                }
                throw;
            }
        }

首先 GetEntriesToSave 方法,这个方法就是和上面的Add呼应,将EFCore加入到内存集合中管理的实体再次读取出来,得到一个entriesToSave集合,也就是需要保存的实体集合,看下其中的部分代码就是将内存集合中的数据得到

            if (!hasDependentTypes)
            {
                var numberOfStates
                    = (returnAdded ? 1 : 0)
                      + (returnModified ? 1 : 0)
                      + (returnDeleted ? 1 : 0)
                      + (returnUnchanged ? 1 : 0);

                if (numberOfStates == 1)
                {
                    if (returnUnchanged)
                    {
                        return _unchangedReferenceMap.Values;
                    }
                    if (returnAdded)
                    {
                        return _addedReferenceMap.Values;
                    }
                    if (returnModified)
                    {
                        return _modifiedReferenceMap.Values;
                    }
                    if (returnDeleted)
                    {
                        return _deletedReferenceMap.Values;
                    }
                }
                if (numberOfStates == 0)
                {
                    return Enumerable.Empty<InternalEntityEntry>();
                }
            }

可以看到将不同状态的实体集合返回,得到了一个需要保存的实体集合数据,那得到需要保存的实体之后,就需要执行数据库命令了;
整体的保存入口方法还是由 DbContext 提供,DbContext.SaveChanges 方法会调用到 BatchExecutor.cs类中的Execute方法

private int Execute(DbContext _, (IEnumerable<ModificationCommandBatch>, IRelationalConnection) parameters)
        {
            var commandBatches = parameters.Item1;
            var connection = parameters.Item2;
            var rowsAffected = 0;
            IDbContextTransaction startedTransaction = null;
            try
            {
                if (connection.CurrentTransaction == null
                    && (connection as ITransactionEnlistmentManager)?.EnlistedTransaction == null
                    && Transaction.Current == null
                    && CurrentContext.Context.Database.AutoTransactionsEnabled)
                {
                    startedTransaction = connection.BeginTransaction();
                }
                else
                {
                    connection.Open();
                }
                foreach (var batch in commandBatches)
                {
                    batch.Execute(connection);
                    rowsAffected += batch.ModificationCommands.Count;
                }
                startedTransaction?.Commit();
            }
            finally
            {
                if (startedTransaction != null)
                {
                    startedTransaction.Dispose();
                }
                else
                {
                    connection.Close();
                }
            }
            return rowsAffected;
        }

上诉代码中,根据得到的命令集合,循环执行命令来执行命令,最后通过事务统一来提交操作,也是确保DbContext内的事务一致性;
至此,我们EfCore的新增操作就简单分析完了,通过EFCore的DbContext来添加实体对象,通过Add方法,此时对象会加入到EFCore的内存管理集合中,不同状态对象不同的管理集合,调用SaveChanges方法保存,此时EFCore会将内存的实体对象读取出来,然后通过数据库事务统一提交;EFCore在整个操作中给我们加入了一层数据缓存层,也就是内存管理(后面会慢慢交流这一层的内容);

二、数据查询 (R)

数据查询的内容上一篇入门文章中我已经说了相关方法,这里再把调用方式同步下
EF 的数据查询分为两种,跟踪查询和非跟踪查询;
1.跟踪查询是默认方式,默认EF查询出来的数据是跟踪模式(也可以手动调整),用于对数据做更新等数据库操作;
2.非跟踪查询模式,此模式用于单纯的数据查询,后续不需要对数据做相关修改,因为不需要对实体做调整的监测,所以会比跟踪查询相对快一些;
两种方式各有利弊,需要根据自己的业务实际需要来选择;
两种模式的文档说明(https://docs.microsoft.com/zh-cn/ef/core/querying/tracking)

var myDbContext = new MyDbContext();
            var list = myDbContext.TestTables.ToList();
            Console.WriteLine($"TestTable Count: {list.Count}");
            if (!list.Any()) return;
            Console.WriteLine($"TestTable Detail ----------------  ");
            foreach (var item in list)
            {
                Console.WriteLine($"ID : {item.Id} , Name : {item.Name}");
            }
            Console.WriteLine($"------------------------");

三、数据更新 (U)

数据更新操作,必须使用跟踪查询得到数据,然后修改得到的实体信息,再通过DbContext的SaveChanges提交数据即可;
三部曲:
1.跟踪查询得到数据
2.修改实体数据
3.DbContext.SaveChanges保存数据

            var myDbContext = new MyDbContext();
            var list = myDbContext.TestTables.AsTracking().ToList();
            var firstEntity = list.FirstOrDefault(p => p.Id == 1);
            if (firstEntity != null) firstEntity.Name = $"{firstEntity.Name} Query_跟踪查询";
            myDbContext.SaveChanges();
            Console.WriteLine($"------------------------");

四、数据删除 (D)

1.使用跟踪查询,将数据查询出来
2.通过DbSet将其Remove(也是再内存集合中做了标记,将其放入了remove集合中)
3.SaveChanges提交保存

            var myDbContext = new MyDbContext();
            var entity = myDbContext.TestTables.FirstOrDefault(p => p.Id == 1);
            if (entity != null)
                myDbContext.TestTables.Remove(entity);
            myDbContext.SaveChanges();

有没有发现问题,(⊙o⊙)? 删除个数据还要查询出来?业务场景中,很多都是前端传入主键ID,执行执行相关表的数据删除,那是否可以使用非跟踪查询模式呢?
验证下 .....

            var myDbContext = new MyDbContext();
            var entity = myDbContext.TestTables.AsNoTracking().FirstOrDefault(p => p.Id == 1);
            if (entity != null)
                myDbContext.TestTables.Remove(entity);
            myDbContext.SaveChanges();

结果当然是可以删除啦,😄
那既然是非跟踪可以删除,也意味着自己构建的实体可以删除,那是否只需要主键就可以删除了 ?

            var myDbContext = new MyDbContext();
            var entity = new TestTable()
            {
                Id = 1
            };
            myDbContext.TestTables.Remove(entity);
            myDbContext.SaveChanges();

如上的代码,确实也是可以的,删除成功 !!!

至此我们完成了EFCore的入门简单操作,基本都是简单的单表操作,只是为了演示整个EFCore的代码结构,大家可以自己上手尝试下,后续我们将开始EFCore相关的骚操作以及一些进阶操作

11-24 01:05