找到一种有效的模式来缓存实体并使用 EF core 跨请求重用它们

问题描述 投票:0回答:1

我不是实体框架专家,我仍在学习如何使用它。 我面临一个问题,我不明白我是否选择了错误的设计,或者有一个简单的解决方案。

这个问题特指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 不适合的?

c# entity-framework caching entity-framework-core
1个回答
0
投票

下面是一个缓存的实现,可以考虑处理查找实体,该实体封装了对缓存的检查,将找到的缓存条目附加到适用的 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 实例并返回它。如果在缓存中找不到该项目,那么我们将加载它,将其添加到缓存中,然后返回它。这处理了由于读取缓存而可能添加城市的情况。

© www.soinside.com 2019 - 2024. All rights reserved.