我不是实体框架专家,我仍在学习如何使用它。 我面临一个问题,我不明白我是否选择了错误的设计,或者有一个简单的解决方案。
这个问题特指EF core 8。
将这些实体视为我的场景的示例:
public class City
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; } = default!;
public ICollection<Student> Students { get; set; } = new List<Student>();
}
public class Student
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; } = default!;
public int Age { get; set; }
public City City { get; set; } = default!;
public int CityId { get; set; }
}
public class ApplicationDbContext : DbContext
{
public DbSet<City> Cities { get; set; }
public DbSet<Student> Students { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
}
假设此应用程序的城市列表不经常更改。在这种情况下,与其每次从数据库中读取城市,不如只读取一次并将其缓存很长时间(例如:1 周)。
ICityCache
服务可用于从配置的缓存层(例如:内存缓存)获取城市实体:
public interface ICityCache
{
Task<City?> GetByName(string name, CancellationToken cancellationToken);
}
最后,考虑一个用于创建新
Student
实体的操作方法,该方法使用 ICityCache
服务从缓存中读取城市实体,并避免额外的数据库调用从数据库中获取城市:
[ApiController]
[Route("api/students")]
public class StudentsController : ControllerBase
{
private readonly ICityCache _cityCache;
private readonly ApplicationDbContext _context;
public StudentsController(ICityCache cityCache, ApplicationDbContext context)
{
_cityCache = cityCache;
_context = context;
}
[HttpPost]
public async Task<IActionResult> Create(CreateStudentCommand command, CancellationToken cancellationToken)
{
var city = await _cityCache.GetByName(command.City, cancellationToken);
if (city is null)
{
return this.BadRequest("The specified City is unknown");
}
// if I don't add this line, the context tracks the city entity with Added state and it tries to INSERT the city when SaveChangesAsync is called
_context.Cities.Attach(city);
var student = new Student
{
Name = command.Name,
Age = command.Age,
City = city,
};
_context.Students.Add(student);
await _context.SaveChangesAsync(cancellationToken);
return this.Ok(new { id = student.Id });
}
}
这段代码可以工作,但是有一个问题。缓存的
Students
实体的 City
属性随着时间的推移不断增长。一旦新的 Student
实体添加到上下文中,Student
实体就会添加到缓存的 City
实体的学生集合中。我想避免这些缓存的实体随着时间的推移而变化并且不断增长。
我该如何解决这个问题?我应该从
Students
类中完全删除 City
导航属性吗?这很奇怪,因为基本上我正在更改实体定义以解决缓存问题。我能做一些更聪明的事情吗?
也许我以错误的方式设计了这段代码,并且这种跨不同请求重用的实体是 EF core 不适合的?
下面是一个缓存的实现,可以考虑处理查找实体,该实体封装了对缓存的检查,将找到的缓存条目附加到适用的 DbContext 实例,并在必要时加载数据。
public interface ILookupCache<TEntity> where TEntity: class // consider a contract interface for identifying supported lookup entities.
{
TEntity GetById(AppDbContext context, int id);
TEntity GetByName(AppDbContext context, string name);
}
// Singleton scoped
public class CityLookupCache : ILookupCache<City>
{
private IList<City>? _cache = null;
City ILookupCache<City>.GetById(AppDbContext context, int id)
{
var city = context.Set<City>()
.Local()
.FirstOrDefault(x => x.Id == id);
if (city != null) return city;
if(_cache == null) refreshCache(context);
city = _cache.FirstOrDefault(x => x.Id == id);
if(city != null)
{
context.Attach(city);
return city;
}
city = context.SingleOrDefault(x => x.Id == id);
if (city != null)
_cache.Add(city);
return city;
}
City ILookupCache<City>.GetByName(AppDbContext context, string name)
{
var city = context.Set<City>()
.Local()
.FirstOrDefault(x => x.Name == name);
if (city != null) return city;
if(_cache == null) refreshCache(context);
city = _cache.FirstOrDefault(x => x.Name == name);
if(city != null)
{
context.Attach(city);
return city;
}
city = context.SingleOrDefault(x => x.Name == name);
if (city != null)
_cache.Add(city);
return city;
}
private void refreshCache(AppDbContext context)
{
_cache = context.Set<City>()
.AsNoTracking()
.ToList();
}
}
这些调用本质上是为给定的查找调用管理缓存。没有显式的“Load”调用来初始化缓存,它会在第一次调用时初始化。这使用传入的提供的 App DbContext 来允许缓存在请求之间充当单例。它在内部处理与 DbContext 实例关联的找到的查找。
它首先要检查的是应用程序 DbContext 是否已经在跟踪请求的城市。如果是这样,它只返回该实例,不需要缓存检查。
第一次调用时,缓存将为空,因此它将使用 AsNoTracking() 查询填充缓存。对于较大的集合(数千条以上记录),可以删除此步骤,只需检查缓存或按请求加载+缓存单个条目(如果不在缓存中)。 (见下面的步骤)
使用现有缓存或新加载的缓存,我们通过 ID 或名称查找该项目,如果找到,则将其附加到给定的 DbContext 实例并返回它。如果在缓存中找不到该项目,那么我们将加载它,将其添加到缓存中,然后返回它。这处理了由于读取缓存而可能添加城市的情况。