You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

POST新增Item时触发SQLite Error 19: 'UNIQUE constraint failed: PosterDto.Id'问题排查求助

问题原因分析

这个错误的核心原因是EF Core默认将PosterDto识别为独立的数据库实体,自动为它创建了一张单独的表,并且把PosterDto.Id设为主键(自带唯一约束)。当同一个用户创建多个Item时,你每次都会新建一个PosterDto实例并关联到Item,EF Core尝试把这个新的PosterDto插入数据库,但用户的Id已经存在于PosterDto表中,于是触发了SQLite的UNIQUE约束错误。

简单说:你以为PosterDto只是Item里的一组数据,但EF把它当成了需要单独存储的实体,重复插入同一个用户的PosterDto就会冲突。

解决方案

根据你的业务需求,这里有三种可行的解决方式,按推荐程度排序:

方案1:将PosterDto配置为「拥有实体(Owned Entity)」(最推荐)

如果PosterDto只是Item的附属信息(不需要被多个Item共享,也不需要单独查询),最佳做法是告诉EF Core把它作为Item的一部分嵌入到Item表中,而不是创建单独的表。

在你的DataContext中重写OnModelCreating方法,添加以下配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 把Poster配置为Item的拥有实体,字段会嵌入Item表
    modelBuilder.Entity<Item>()
        .OwnsOne(item => item.Poster);
}

配置后,EF会在Item表中生成类似Poster_IdPoster_DisplayNamePoster_Username这样的列,不再需要单独的PosterDto表。每次新增Item时,用户信息会直接存在Item的这些字段里,不会有重复插入的问题。

方案2:复用已存在的PosterDto实体

如果你确实需要PosterDto作为独立实体(比如多个Item需要关联同一个Poster记录),那么在创建Item前,先检查数据库中是否已经存在对应用户的PosterDto,不存在再创建:

修改Handler中的代码:

public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
    var user = await _userManager.FindByNameAsync(_userAccessor.GetCurrentUsername());
    if (user == null)
    {
        throw new RestException(HttpStatusCode.Unauthorized, new { user = "Not found" });
    }

    // 先查询数据库中是否已有该用户的PosterDto
    var poster = await _context.Set<PosterDto>()
        .FirstOrDefaultAsync(p => p.Id == user.Id, cancellationToken);
    
    // 如果不存在,再创建新的
    if (poster == null)
    {
        poster = new PosterDto {
            Id = user.Id,
            DisplayName = user.DisplayName,
            Username = user.UserName,
            Photo = user.Photo
        };
        _context.Set<PosterDto>().Add(poster);
    }

    var item = new Item {
        Id = request.Id,
        Title = request.Title,
        Description = request.Description,
        Date = DateTime.Now,
        Latitude = request.Latitude,
        Longitude = request.Longitude,
        Image = request.Image,
        Poster = poster
    };

    _context.Items.Add(item);
    var success = await _context.SaveChangesAsync(cancellationToken) > 0;

    if (success)
    {
        return Unit.Value;
    }

    throw new NotImplementedException("Failed to create item");
}

这种方式需要确保EF Core已经正确映射PosterDto为实体(默认会自动映射,只要它在DbContext的DbSet中,或者被导航属性引用)。

方案3:直接关联AppUser实体,去掉PosterDto

其实你已经有AppUser实体了,完全可以直接让Item关联AppUser,不需要额外的PosterDto

第一步:修改Item类

public class Item {
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime Date { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public string Image { get; set; }
    
    // 直接关联AppUser,添加外键字段
    public string PosterId { get; set; }
    public AppUser Poster { get; set; }
    
    public ICollection<Message> Messages { get; set; } = new List<Message>();
}

第二步:修改Handler代码

var item = new Item {
    Id = request.Id,
    Title = request.Title,
    Description = request.Description,
    Date = DateTime.Now,
    Latitude = request.Latitude,
    Longitude = request.Longitude,
    Image = request.Image,
    PosterId = user.Id // 直接设置外键,不需要新建实体
};

这种方式最简洁,因为复用了已有的用户实体,避免了额外的Dto映射和实体冲突问题。

总结

最可能的场景是你把PosterDto当成了数据传输对象,但EF Core把它当成了数据库实体。推荐用方案1(拥有实体)来解决,既保留了Dto的封装性,又不会产生额外的表和约束问题。如果你的业务需要共享Poster数据,再考虑方案2或3。

内容的提问来源于stack exchange,提问作者VueDev2020

火山引擎 最新活动