如何解决拥有类型的延迟加载 不使用自有类型的选项按推荐顺序覆盖InternalDbSet方法提供一个using System; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Exten
我正在朝着使用实体框架核心的域驱动设计迈出第一步。我有一个User
实体,在简化版本中,它只有Id
和ProfilePhoto
。但是,我想将个人资料照片存储在另一个表中,这就是为什么我创建了一个包含该个人资料照片并以这种方式配置的拥有类型的原因:
用户:
public class User
{
private int id;
public int Id => this.id;
//private UserProfilePhoto userProfilePhoto;
public virtual UserProfilePhoto UserProfilePhoto { get; set; }
private User()
{
}
public static User Create(byte[] profilePhoto)
{
var user = new User();
user.UserProfilePhoto = new UserProfilePhoto(profilePhoto);
return user;
}
public void SetProfilePhoto(byte[] profilePhoto)
{
this.UserProfilePhoto = new UserProfilePhoto(profilePhoto);
}
}
UserProfilePhoto:
public class UserProfilePhoto
{
public byte[] ProfilePhoto { get; private set; }
public UserProfilePhoto(byte[] profilePhoto)
{
this.ProfilePhoto = profilePhoto;
}
}
DbContext配置:
public class ModelContext : DbContext
{
public ModelContext(DbContextOptions<ModelContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
OnUserModelCreating(modelBuilder);
}
protected void OnUserModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasKey(u => u.Id);
modelBuilder.Entity<User>()
.Property(u => u.Id)
.HasField("id");
modelBuilder.Entity<User>()
.OwnsOne(u => u.UserProfilePhoto,builder =>
{
builder.ToTable("UserProfilePhoto");
builder.Property(u => u.ProfilePhoto)
.IsRequired();
});
}
}
我选择使用“拥有的”类型,因为我希望个人资料照片只能从用户实体访问。通过一对一映射,例如,我仍然可以使用UserProfilePhoto
访问context.Set<UserProfilePhoto>()
表,并且,据我了解的DDD聚合,这可能意味着跳过User
业务逻辑。
因此,我进行了迁移,数据库模型就像我期望的那样:UserProfilePhoto
表,其主键和外键为User.Id
。
很明显,在我的查询中,我不想每次都加载整个User
实体,因此我未成功启用延迟加载。这是我在单元测试中尝试过的代码:
protected ModelContext GetModelContext(DbContextOptionsBuilder<ModelContext> builder)
{
builder
.UseLoggerFactory(loggerFactory)
.UseLazyLoadingProxies()
.EnableDetailedErrors();
var ctx = new ModelContext(builder.Options);
ctx.Database.EnsureCreated();
return ctx;
}
[TestMethod]
public async Task TestMethod1()
{
var builder = new DbContextOptionsBuilder<ModelContext>()
.UseSqlServer(...);
var ctx = this.GetModelContext(builder);
var user = User.Create(new byte[] { });
try
{
await ctx.Users.AddAsync(user);
await ctx.SaveChangesAsync();
var users = ctx.Users;
foreach (var u in users)
{
Console.WriteLine(u.Id);
}
}
finally
{
ctx.Users.Remove(user);
await ctx.SaveChangesAsync();
ctx.Database.EnsureDeleted();
}
}
这是生成的SQL:
SELECT [u].[Id],[u0].[UserId],[u0].[ProfilePhoto]
FROM [Users] AS [u]
LEFT JOIN [UserProfilePhoto] AS [u0] ON [u].[Id] = [u0].[UserId]
我不确定它是否有效,但是注入ILazyLoader
并不是我的解决方案,另一方面,这就像弄脏模型一样。
我的疑问是,拥有的类型不会通过实际的导航属性绑定到主体实体,因此不支持为它们创建代理。
我的方法有什么问题?是DDD吗?如果是这样,我该如何懒惰地加载拥有的实体?
我发现与此相关的issue on Github,尽管它不能回答我的问题。
修改
我的目标是阻止从EF api访问UserProfilePhoto
表(请参阅注释)。如果我能够做到这一点,那么保护我的UserProfilePhoto
类并将其封装在User
类中将很容易,就像这样:
User
...
protected virtual UserProfilePhoto UserProfilePhoto { get; set; }
public void SetProfilePhoto(byte[] profilePhoto)
{
this.UserProfilePhoto.SetProfilePhoto(profilePhoto);
}
public byte[] GetProfilePhoto()
{
return this.UserProfilePhoto.ProfilePhoto;
}
...
我尝试了一对一映射的代码,即使在延迟加载的情况下也可以完美地工作。我该如何仅使用拥有类型?还有其他方法吗?
解决方法
当所有者被加载时(从Owned Entity Types: Querying owned types),EF Core会自动加载拥有的类型
在查询所有者时,默认情况下将包括拥有的类型。即使拥有的类型存储在单独的表中,也不必使用Include方法。
因此,使用拥有的类型不能满足仅按需加载的要求。
(您可以修改//frameOutputNode = graph.CreateFrameOutputNode();
//mediaInput.AddOutgoingConnection(frameOutputNode);
//frameOutputNode.OutgoingGain = 4;
等,但这在很大程度上不受支持,不太可能在所有情况下都有效,并且可能随时中断。)
不使用自有类型的选项(按推荐顺序)
- 覆盖
Metadata.PrincipalToDependent.SetIsEagerLoaded(false)
,DbContext.Set<>()
等,如果调用不当则抛出 - 实施传统的自定义工作单元和存储库模式,该模式可让您完全控制所公开的API(交易具有控制灵活性)
- 尽早在查询管道中添加一个表达式访问者(注册
DbContext.Find()
并从IQueryTranslationPreprocessorFactory
派生),如果在查询中的任何地方都使用了RelationalQueryTranslationPreprocessorFactory
,则会抛出该表达式访问者 - 提供自己的
DbSet<UserProfilePhoto>
(和IDbSetSource
)实现(内部),如果调用不当则抛出
覆盖InternalDbSet
方法
通常,仅覆盖DbContext
,DbContext.Set<>()
等应该是最简单的解决方案。您可以使用自定义属性修饰您不想直接查询的类型,然后只需检查DbContext.Find()
等是否尚未使用该自定义属性修饰即可。
为了便于维护,可以将所有重写的方法移至基类,该类也可以执行运行时检查以确保所有有问题的方法都被重写(当然,这些检查也可以通过单元测试来完成) )。
以下是演示此方法的示例:
TEntity
提供一个using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class DontRootQueryMeAttribute : Attribute
{
}
public class User
{
public int Id { get; private set; }
public virtual UserProfilePhoto UserProfilePhoto { get; set; }
public static User Create(byte[] profilePhoto)
{
var user = new User
{
UserProfilePhoto = new UserProfilePhoto(profilePhoto)
};
return user;
}
}
[DontRootQueryMeAttribute]
public class UserProfilePhoto
{
public int Id { get; set; }
public byte[] ProfilePhoto { get; private set; }
public UserProfilePhoto(byte[] profilePhoto)
{
ProfilePhoto = profilePhoto;
}
}
public abstract class ModelContextBase : DbContext
{
private static readonly string[] OverriddenMethodNames =
{
nameof(DbContext.Set),nameof(DbContext.Query),nameof(DbContext.Find),nameof(DbContext.FindAsync),};
static ModelContextBase()
{
var type = typeof(ModelContextBase);
var overriddenMethods = type
.GetRuntimeMethods()
.Where(
m => m.IsPublic &&
!m.IsStatic &&
OverriddenMethodNames.Contains(m.Name) &&
m.GetRuntimeBaseDefinition() != null)
.Select(m => m.GetRuntimeBaseDefinition())
.ToArray();
var missingOverrides = type.BaseType
.GetRuntimeMethods()
.Where(
m => m.IsPublic &&
!m.IsStatic &&
OverriddenMethodNames.Contains(m.Name) &&
!overriddenMethods.Contains(m))
.ToArray();
if (missingOverrides.Length > 0)
{
throw new InvalidOperationException(
$"The '{nameof(ModelContextBase)}' class is missing overrides for {string.Join(",",missingOverrides.Select(m => m.Name))}.");
}
}
private void EnsureRootQueryAllowed<TEntity>()
=> EnsureRootQueryAllowed(typeof(TEntity));
private void EnsureRootQueryAllowed(Type type)
{
var rootQueriesAllowed = type.GetCustomAttribute(typeof(DontRootQueryMeAttribute)) == null;
if (!rootQueriesAllowed)
throw new InvalidOperationException($"Directly querying for '{type.Name}' is prohibited.");
}
public override DbSet<TEntity> Set<TEntity>()
{
EnsureRootQueryAllowed<TEntity>();
return base.Set<TEntity>();
}
public override DbQuery<TQuery> Query<TQuery>()
{
EnsureRootQueryAllowed<TQuery>();
return base.Query<TQuery>();
}
public override object Find(Type entityType,params object[] keyValues)
{
EnsureRootQueryAllowed(entityType);
return base.Find(entityType,keyValues);
}
public override ValueTask<object> FindAsync(Type entityType,params object[] keyValues)
{
EnsureRootQueryAllowed(entityType);
return base.FindAsync(entityType,object[] keyValues,CancellationToken cancellationToken)
{
EnsureRootQueryAllowed(entityType);
return base.FindAsync(entityType,keyValues,cancellationToken);
}
public override TEntity Find<TEntity>(params object[] keyValues)
{
EnsureRootQueryAllowed<TEntity>();
return base.Find<TEntity>(keyValues);
}
public override ValueTask<TEntity> FindAsync<TEntity>(params object[] keyValues)
{
EnsureRootQueryAllowed<TEntity>();
return base.FindAsync<TEntity>(keyValues);
}
public override ValueTask<TEntity> FindAsync<TEntity>(object[] keyValues,CancellationToken cancellationToken)
{
EnsureRootQueryAllowed<TEntity>();
return base.FindAsync<TEntity>(keyValues,cancellationToken);
}
// Add other overrides as needed...
}
public class ModelContext : ModelContextBase
{
public DbSet<User> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63887500_01")
.UseLoggerFactory(LoggerFactory.Create(b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
OnUserModelCreating(modelBuilder);
}
protected void OnUserModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(
entity =>
{
entity.HasOne(e => e.UserProfilePhoto)
.WithOne()
.HasForeignKey<UserProfilePhoto>(e => e.Id);
});
}
}
internal static class Program
{
private static void Main()
{
var accessingSetThrows = false;
using (var ctx = new ModelContext())
{
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
var user = User.Create(new byte[] { });
ctx.Users.Add(user);
ctx.SaveChanges();
// Make sure,that UserProfilePhoto cannot be queried directly.
try
{
ctx.Set<UserProfilePhoto>()
.ToList();
}
catch (InvalidOperationException)
{
accessingSetThrows = true;
}
Debug.Assert(accessingSetThrows);
}
// No eager loading by default with owned type here.
using (var ctx = new ModelContext())
{
var users = ctx.Users.ToList();
Debug.Assert(users.Count == 1);
Debug.Assert(users[0].UserProfilePhoto == null);
}
// Explicitly load profile photo.
using (var ctx = new ModelContext())
{
var users = ctx.Users.ToList();
ctx.Entry(users[0]).Reference(u => u.UserProfilePhoto).Load();
Debug.Assert(users.Count == 1);
Debug.Assert(users[0].UserProfilePhoto != null);
}
}
}
}
实现
表达式访问者可用于解决问题,方法是使用IQueryTranslationPreprocessorFactory
实现在查询中搜索特定表达式,只有在调用新的IQueryTranslationPreprocessorFactory
扩展方法并抛出该异常时才添加该表达式,如果丢失,并且正在查询非根实体。实际上,这应该足以确保团队中没有人偶然查询非根对象。
(您还可以将内部类实例作为常量参数添加到方法调用表达式中,然后在表达式访问者中对其进行求值,以确保调用者确实对{{1 }}方法。但这只是锦上添花,在实践中是不必要的,因为开发人员无论如何都可以使用反射来绕过任何访问限制。因此,我不会为实现这一问题而烦恼。
这里是实现(使用自定义界面而不是自定义属性来标记不应直接查询的实体):
InternalQuery()
,
字节真的应该是域的一部分吗?您实际上在用户配置文件上下文中的那些字节上运行任何业务逻辑吗?真的有一种用例,您想从User
AR内部访问字节吗?
如果不是这样,那么将字节存储与照片的元数据解耦,并引入具有ProfilePhoto
属性的storageUrl/storageId
VO来定位字节是更有意义的。
请不要忘记您的域模型应该为命令而不是查询和表示层设计。
当然,现在在将字节和AR的数据存储在数据库中时,您不容易拥有ACID属性,但通常很容易通过清理过程来解决。
如果您不需要User
中的个人资料照片的元数据来执行业务规则,那么您也可以考虑将ProfilePhoto
设为自己的AR。
最后,我认为尝试防止ORM滥用是不必要的。应该将ORM视为低级API,永远不要直接将其用于更改AR状态。我认为可以安全地假设开发人员将有足够的严格性来遵守该规则,就像他们应该尊重整个系统的体系结构一样。如果他们不这样做,那么您会有更大的问题。如果可以轻松地向成员添加私有修饰符,那么可以肯定,但这似乎需要付出很多努力,因此我将采取务实的方式...
,我找到了一个临时解决方案:
modelBuilder.Entity<User>()
.OwnsOne(u => u.UserProfilePhoto,builder =>
{
builder.Metadata.IsOwnership = false;
builder.Metadata.IsRequired = false;
builder.Metadata.PrincipalToDependent.SetIsEagerLoaded(false);
builder.ToTable("UserProfilePhoto");
builder.Property(u => u.ProfilePhoto)
.IsRequired();
});
我不喜欢它,我想EF允许您以其他更清晰的方式进行配置。我不接受这个答案,希望其他人可以指出正确的方向。
编辑:代理以这种方式工作,但是当删除User
时,与UserProfilePhoto
的关联被切断:
实体“
User
”和“UserProfilePhoto
”之间的关联 键值'{UserId:1}'已被切断,但关系为 标记为“必需”或隐式必需,因为 外键不可为空。如果附属/子实体应该是 在切断必要的关系后将其删除,然后设置 关系以使用级联删除。
我什至尝试通过元数据指定DeleteBehaviour.Cascade
选项,但这可能会破坏内部约束。
此外,现在可以通过DbContext.Set<UserProfilephoto>()
来访问它,这不是我想要的。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。