很久没认真聊 NHibernate 了。最近在给模拟项目X做多租户数据隔离改造时一个平时几乎没人注意的特性把我从一堆if else 切换连接串的烂泥里捞了出来——就是 entity-name。提到 NHibernate大家脑子里冒出来的通常是类到表映射、HQL、一级缓存、二级缓存、SessionFactory这些经典概念。多数人写了好几年 .NET可能都没给同一个实体类单独起过映射别名觉得完全没必要。但当你真正遇到同一个 POCO 类要映射到多张结构相似但物理隔离的业务表或者同一张表在不同调用方眼里需要有不同的持久化规则这类需求时entity-name 会是你见过的最干净的一把手术刀。这篇文章我把 entity-name 的来龙去脉、配置方式、API 用法、真实落地案例和踩坑实况一次讲透。内容全部来自我近期的实操复盘不是翻译文档适合正在用 NHibernate 做中大型项目、或准备把老项目往多租户架构上迁移的同学参考。1. 从默认映射到 entity-name这个特性到底在解决什么问题1.1 先搞清楚 NHibernate 默认的实体名是什么NHibernate 里每个持久化类在映射体系中都会对应一个实体名entity-name。在不做任何特殊配置的情况下这个实体名默认就是类的完全限定名或者你在映射文件中指定的类名。举个例子你有一个普通的类public class Customer { public virtual Guid Id { get; set; } public virtual string Name { get; set; } }对应的映射文件是hibernate-mapping xmlnsurn:nhibernate-mapping-2.2 class nameMyApp.Domain.Customer, MyApp tableCustomers id nameId columnId typeGuid generator classguid/ /id property nameName columnName length100/ /class /hibernate-mapping这时候实体名就是MyApp.Domain.Customer。你用session.GetCustomer(id)也好用session.Get(typeof(Customer), id)也好NHibernate 内部的解析路径都归结到同一个实体名。这种默认模型对绝大多数项目完全够用。但它隐含了一个强约束一个实体类在同一个 SessionFactory 里只能对应一个映射定义、一张主表。当你需要打破这个约束时事情就变得麻烦。1.2 三个典型场景逼得你不得不用 entity-name我实际遇到过的场景主要有三类大家可以对照一下自己的项目里有没有类似的苗头。场景一多租户物理表隔离这是最常见的需求。比如你做了一个客服工单系统接入多个运营方每个运营方要求工单数据物理隔离不能都塞进一张大表里。最简单的做法是每租户独立一张表运营方 A 的数据放Ticket_A运营方 B 的数据放Ticket_B但业务代码里Ticket类的结构完全一样。如果不引入 entity-name你就得写两个映射文件、甚至两个 SessionFactory代码里再通过分支去切换非常痛苦。用 entity-name类可以只有一个映射声明两个实体名即可。场景二同一实体在不同上下文下的差异化持久化规则有时候连表都不变但不同调用方对数据的过滤规则不同。比如管理端要看到软删除的所有记录客户端只能看到未删除的。默认情况下一个实体就一套映射你要么用拦截器做全局过滤要么在每个查询里手动加条件。而通过两个 entity-name 指向同一张表、不同where过滤条件能把这个差异化收敛在映射层。场景三使用非 POCO 的持久化对象动态模型NHibernate 支持不绑定实体类直接用IDictionary存数据。这种动态模型必须配合 entity-name 使用。虽然这种用法比较少见了但它是理解整个特性底层的钥匙。这三种场景的共同本质是把类和实体名解耦让映射体系出现一个更细的粒度——实体名。你操作的不再是单一的 CLR 类型而是一组可供选择的映射标识。1.3 entity-name 与多租户架构的关系很多读过 Hibernate 官方文档的老同学应该记得Hibernate 在多租户章节里专门提过一种轻量级方案通过动态切换 entity-name 来实现每个租户对应一套持久化单位。NHibernate 这边从 3.x 开始完整支持了 entity-name 的持久化 API。它的好处在于不需要维护多个 SessionFactory对配置和启动开销友好同一个事务边界内可以同时操作多个租户的实体不会因为跨工厂而陷入分布式事务对现有代码侵入小只是在获取/保存那一步多传一个字符串参数当然它也有上限。物理隔离做到表这一层没问题但如果每个租户的表结构差异极大或者缓存策略完全不同仍然建议走独立的 SessionFactory。entity-name 适合结构相同或高度相似只需物理隔离的场景。2. entity-name 的配置方式与映射文件解析2.1 在 hbm.xml 中配置 entity-name 的基本姿势用 XML 映射时entity-name 是class节点上的一个可选属性。核心区别在于如果只写了nameCLR 类名实体名默认等于类名如果同时写了entity-name实体名就由你显式指定。看下面这个例子同一个类两套映射对应两张不同表hibernate-mapping xmlnsurn:nhibernate-mapping-2.2 class nameMyApp.Domain.OperatorTicket, MyApp entity-nameTenantA_Ticket tableTicket_TenantA id nameId columnId typeGuid generator classguid/ /id property nameTitle columnTitle length200/ property nameContent columnContent typeStringClob/ property nameCreatedAt columnCreatedAt typeDateTime/ /class class nameMyApp.Domain.OperatorTicket, MyApp entity-nameTenantB_Ticket tableTicket_TenantB id nameId columnId typeGuid generator classguid/ /id property nameTitle columnTitle length200/ property nameContent columnContent typeStringClob/ property nameCreatedAt columnCreatedAt typeDateTime/ /class /hibernate-mapping这里有几个关键点要强调两个class共用同一个name实体名不同映射的主表不同。这是最基本的形式。如果两个映射的属性映射基本一致只是表名不同很多人会考虑去继承映射配置来减少重复。NHibernate 支持union-subclass、join等高级映射但 entity-name 在继承体系中和它们的配合需要额外小心后面专门讲。entity-name 相当于映射的逻辑主键在 SessionFactory 中唯一。同一个 CLR 类型允许注册多个实体名但实体名之间不允许重复。提示entity-name 的命名没有强制规则。但建议用业务逻辑名租户标识或环境标识的组合方式比如Ticket_ByTenantA、Ticket_ByTenantB而不是随意起asdf。在生产系统里实体名会出现在查询日志、缓存 Region、异常消息中可读性非常重要。2.2 用 FluentNHibernate 配置 entity-name如果你项目里用的是流式配置比如 FluentNHibernate同样能设置实体名。在ClassMapT子类里可以这样写public class TicketTenantAMap : ClassMapTicket { public TicketTenantAMap() { Entity(TenantA_Ticket); // 关键指定 entity-name Table(Ticket_TenantA); Id(x x.Id).GeneratedBy.Guid(); Map(x x.Title).Length(200); Map(x x.Content).CustomTypeStringClob(); Map(x x.CreatedAt); } } public class TicketTenantBMap : ClassMapTicket { public TicketTenantBMap() { Entity(TenantB_Ticket); Table(Ticket_TenantB); Id(x x.Id).GeneratedBy.Guid(); Map(x x.Title).Length(200); Map(x x.Content).CustomTypeStringClob(); Map(x x.CreatedAt); } }注册的时候两个 IClassMetadata 都加到同一个 Configuration 里var cfg new NHibernate.Cfg.Configuration(); cfg.AddMapping(new TicketTenantAMap().GetClassMapping()); cfg.AddMapping(new TicketTenantBMap().GetClassMapping()); var sessionFactory cfg.BuildSessionFactory();注意一点FluentNHibernate 的Entity()方法在版本之间有过细微调整如果引入后发现生成的映射 XML 里没有 entity-name 属性检查一下你的 FluentNHibernate 版本尽量用 2.x 以上。2.3 通过 API 按实体名操作对象配置只是第一步真正干活靠的是 ISession 上那一组带 entityName 参数的重载方法。假设上面两份映射已经加载到 SessionFactory那么操作就可以这么写using (var session sessionFactory.OpenSession()) using (var tx session.BeginTransaction()) { // 保存指定实体名 领域对象 var ticketA new Ticket { Title 登录页面报错, Content 用户反馈点击登录后一直转圈, CreatedAt DateTime.Now }; session.Save(TenantA_Ticket, ticketA); // 按实体名加载 var loadedA session.Get(TenantA_Ticket, ticketA.Id); // 按实体名创建 HQL 查询 var list session.CreateQuery(from TenantA_Ticket).ListTicket(); tx.Commit(); }注意这里 HQL 语句里的TenantA_Ticket不是表名也不是 CLR 类名而是实体名。NHibernate 解析 HQL 时遇到from TenantA_Ticket会先去映射元数据里找这个实体名对应的持久化类再根据该实体名的元数据生成 SQL。还有几个比较常用的重载// 删除 session.Delete(TenantA_Ticket, loadedA); // 加载代理 var proxy session.Load(TenantA_Ticket, id); // 刷新 session.Refresh(TenantA_Ticket, loadedA);这些方法的第二个参数或者字符串参数指向的就是实体名。如果用强类型的泛型版本是拿不到实体名这个参数的这一点很多新手会懵。2.4 关联映射里如何使用 entity-name单个实体操作简单真正复杂的是关联关系。当Ticket包含一个Operator处理人引用时用什么实体名去关联直接决定数据路由到哪张表。一个常见的配置是在many-to-one中显式指定entity-name。class nameMyApp.Domain.Ticket, MyApp entity-nameTenantA_Ticket tableTicket_TenantA ... many-to-one nameOperator classMyApp.Domain.Operator, MyApp entity-nameTenantA_Operator columnOperatorId/ /class class nameMyApp.Domain.Ticket, MyApp entity-nameTenantB_Ticket tableTicket_TenantB ... many-to-one nameOperator classMyApp.Domain.Operator, MyApp entity-nameTenantB_Operator columnOperatorId/ /class对应Operator类的两个映射也用 entity-name 区分一个指向Operator_TenantA一个指向Operator_TenantB。这样设计之后从Ticket的一方加载关联时NHibernate 会按映射定义的 entity-name 去加载对应的Operator实例不会串租户。集合关联set标签也一样在one-to-many上指定entity-name即可。关键教训一旦开始用实体名区分租户所有关联映射里的class属性都不够用了必须检查并补充entity-name。漏掉任何一个关联上的 entity-name就可能把 A 租户的工单关联到 B 租户的处理人数据上这是数据安全事故级别的 bug。3. 实操复盘用 entity-name 做一个真实的多租户工单隔离3.1 业务场景建模先交代背景。模拟项目X是个客服工单应用接入了 A、B 两家运营方。需求非常明确A 运营方的工单数据只允许落Ticket_TenantA表B 运营方的工单数据只允许落Ticket_TenantB表业务代码层不希望在查询时到处写 where TenantId xxx每个请求只服务一个租户但一个事务边界内可能同时读写多个租户数据这个背景本质上就是一个典型的共享代码、物理隔离表的多租户场景。项目里工单实体类叫Ticket原来是很普通的一张Tickets表全局公用。改造时如果硬拆 SessionFactory等于把 A/B 两套配置、两套缓存全部隔离成本太高。用 entity-name 则最小化了配置面和代码面。3.2 映射文件与实体对象的设计先是领域实体它依然是普通的 POCOpublic class Ticket { public virtual Guid Id { get; set; } public virtual string Title { get; set; } public virtual string Content { get; set; } public virtual DateTime CreatedAt { get; set; } public virtual Operator Assignee { get; set; } } public class Operator { public virtual Guid Id { get; set; } public virtual string DisplayName { get; set; } public virtual string Mobile { get; set; } }映射文件分为 A 租户和 B 租户两份。为避免手写重复我把公共部分的属性配置抽成了一个基类映射然后用两个子类映射卡片分别覆写表名和 entity-name。但这里要提醒一点XML 映射对继承配置的支持比较粗糙我最后实际用的是完全独立的两个 class 映射块宁可多写一段配置也不去搞过度抽象——因为映射文件一旦继承关系过多调试时很难一眼看清最终生效的表名和实体名。最终映射的关键部分如下!-- Ticket A 租户 -- class nameMyApp.Domain.Ticket, MyApp entity-nameTicket_TenantA tableTicket_TenantA id nameId columnId typeGuidgenerator classguid//id property nameTitle columnTitle length200/ property nameContent columnContent typeStringClob/ property nameCreatedAt columnCreatedAt typeDateTime/ many-to-one nameAssignee classMyApp.Domain.Operator, MyApp entity-nameOperator_TenantA columnAssigneeId / /class !-- Ticket B 租户 -- class nameMyApp.Domain.Ticket, MyApp entity-nameTicket_TenantB tableTicket_TenantB id nameId columnId typeGuidgenerator classguid//id property nameTitle columnTitle length200/ property nameContent columnContent typeStringClob/ property nameCreatedAt columnCreatedAt typeDateTime/ many-to-one nameAssignee classMyApp.Domain.Operator, MyApp entity-nameOperator_TenantB columnAssigneeId / /class处理人Operator的映射类似两个 class 块分别指定Operator_TenantA和Operator_TenantB。这样一份映射清单放到某个程序集里之后BuildSessionFactory 会自动加载。建议把所有映射文件放在一个独立类库里通过cfg.AddAssembly(...)一次性加载避免漏注册。3.3 动态切换 entity-name 的仓储封装业务代码里最需要的是根据当前租户拿到对应实体名。我在项目里做了一个非常轻量的解析器public static class TenantMapping { public const string TicketA Ticket_TenantA; public const string TicketB Ticket_TenantB; public const string OperatorA Operator_TenantA; public const string OperatorB Operator_TenantB; public static string TicketName(string tenantCode) { return tenantCode TenantA ? TicketA : TicketB; } public static string OperatorName(string tenantCode) { return tenantCode TenantA ? OperatorA : OperatorB; } }然后是一个替代原来IRepositoryTicket的仓储方法public class TicketRepository { private readonly ISession _session; private readonly string _tenantCode; public TicketRepository(ISession session, string tenantCode) { _session session; _tenantCode tenantCode; } public Guid Save(Ticket ticket) { var entityName TenantMapping.TicketName(_tenantCode); _session.Save(entityName, ticket); return ticket.Id; } public Ticket Get(Guid id) { var entityName TenantMapping.TicketName(_tenantCode); return _session.Get(entityName, id) as Ticket; } public IListTicket List(DateTime from, DateTime to) { var entityName TenantMapping.TicketName(_tenantCode); var hql $from {entityName} t where t.CreatedAt between :from and :to; return _session.CreateQuery(hql) .SetParameter(from, from) .SetParameter(to, to) .ListTicket(); } }用起来就是一个典型的构造注入// 从当前请求上下文解析出租户标识 var tenantCode ResolveTenantCodeFromHttpContext(); using (var session sessionFactory.OpenSession()) using (var tx session.BeginTransaction()) { var repo new TicketRepository(session, tenantCode); var ticket repo.Get(ticketId); repo.Save(ticket); tx.Commit(); }这个封装的核心价值在于业务代码里再也看不到任何一个具体的表名租户路由完全收敛到TenantMapping和仓储层。就算以后接入新的运营方 C只需要增加TenantC_Ticket的映射和映射常量仓储层几乎不用动。3.4 事务与会话级别必须注意的边界这是我改造过程中踩的最深的一个坑专门单独拿出来讲。NHibernate 的 ISession 是一级缓存它会缓存当前会话加载过的所有实体。在单租户场景下这不是问题但在多租户动态切换的场景下一级缓存可能变成串味源头。举个例子请求 1 中用Ticket_TenantA加载了一个 Id123 的工单放在 session 缓存里。同一个 session 里后续又用Ticket_TenantB加载了一个 Id123 的另一个工单。明明是两个不同实体名对应的两行数据但因为底层 CLR 类型都是Ticket一级缓存中发生冲突时NHibernate 会选择把后加载的实例覆盖到同一位置或者抛出NonUniqueObjectException。解决办法是严格遵循一次请求一个 Session、一个租户上下文的原则// 正确姿势每个请求/工作单元内用独立的 ISession using (var session sessionFactory.OpenSession()) using (var tx session.BeginTransaction()) { // 全程只服务一个租户 }如果确实需要在同一个事务里操作多个租户的数据不要用同一个 ISession 来回切实体名而是用两个独立的 Session各自负责一个实体名。这是我在实际项目里试出来的最稳的姿势。提示如果某个请求的服务方法里需要同时读取 A、B 两个租户的数据做对比建议拆成两个 Repository 实例各自持有独立的 ISession。跨租户的数据对比在内存里做也不要共用一个 Session 的大缓存。3.5 二级缓存 Region 要按实体名隔离如果你的项目开了二级缓存光在操作层区分实体名还不够缓存 Region 也要分开。NHibernate 默认的二级缓存 Region 是跟实体名走的但在配置上很多人会图省事把两个实体名的缓存 Region 都设成同一个地方。这就可能导致A 租户的工单被缓存后B 租户的查询拿到缓存里的实体缓存失效清理时一个 Region 清掉另一个实体名的数据也被误伤正确的做法是在映射文件里显式指定 Regionclass nameMyApp.Domain.Ticket, MyApp entity-nameTicket_TenantA tableTicket_TenantA cache usageread-write regionTicket_TenantA_Region/ ... /class class nameMyApp.Domain.Ticket, MyApp entity-nameTicket_TenantB tableTicket_TenantB cache usageread-write regionTicket_TenantB_Region/ ... /class在代码里添加查询缓存时也一样为每个租户单独指定 cache regionquery.SetCacheable(true) .SetCacheRegion(entityName _Region);单独划分缓存 Region 还有一个额外好处当 B 租户的数据频繁更新时不会把 A 租户的缓存也全盘失效。特别是大促或者批量导入场景这个收益非常可观。4. 常见问题与排查技巧实录4.1 Unknown entity name 问题这个异常是我调试时排队率最高的。报错信息类似NHibernate.MappingException: Unknown entity name: Tickets_TenantA排查思路按顺序走检查映射文件有没有被加载。很多人用AddAssembly加载程序集时没有把包含映射文件的类库作为参数传进去导致整个程序集的映射都没被读取。检查大小写。实体名是大小写敏感的。ticket_TenantA和Ticket_TenantA是两个名字。检查命名空间前缀。如果你在 HQL 里用了完整的命名空间写实体名但配置时没写前缀就会找不到。检查显式还是隐式。如果 XML 里entity-name没写但你在代码里当成写了来用也会报错。默认情况下除非你真的配置了 entity-name否则实体名就是类名不要凭空去猜一个。我排查时最喜欢的操作是写一个小工具打印出 SessionFactory 中所有注册实体名var metadata sessionFactory.GetAllClassMetadata(); foreach (var key in metadata.Keys) { Console.WriteLine(key); }这样一眼就能看出哪几个实体名是确实注册成功的少走很多弯路。4.2 泛型方法与实体名之间的类型转换陷阱session.GetT(id)在内部拿到的实体名匹配是第一个匹配该 CLR 类型的实体。如果你注册了两个 entity-name泛型版本永远是二选一结果不可控。所以只要注册了多个 entity-name就不要在数据访问层用泛型 Get/Save。我这里有一个比较尴尬的真实案例某个仓储方法用的是session.GetTicket(id)上线后发现数据时而来自 A 表时而来自 B 表完全随机。当时怀疑是缓存问题排查到最后才发现是泛型方法把所有实体名都当成一个来处理。解决方案非常简单把泛型 API 替换成带 entityName 的版本。哪怕是通用的ISession扩展方法参数里也要强制传入 entity-name 字符串。4.3 关联加载时 class 属性不生效的 bug另一个在文档里很难搜到的问题是many-to-one里只写了class没有写entity-name。这种情况下NHibernate 会使用该 CLR 类型的默认实体名去加载关联对象。如果一个类的默认实体名只对应到一个 table那没问题但如果这个类存在多个 entity-nameNHibernate 就会报不确定加载哪个实体或者直接加载到错误的表。实践中我在排查一个 B 租户工单处理人加载异常时发现的规律是报错往往不在第一次加载而是在 Session 里已经存在另一租户同类型实体时数据错乱不会提示但生成的 SQL 会指向错误的表我的建议是给所有关联配置做一次实体名完整性审查。简单粗暴但有效// 检查映射中的关联字段强制要求 all association 都配置了 entity-name否则启动报错可以写一个启动时的配置校验器遍历每个 IClassMetadata 的关联属性排查有没有缺失 entity-name 的情况。越早暴露问题越好解决。4.4 Session.Clear 在切换租户时的作用如果因为一些历史原因你确实不得不在一个 Session 里切换租户一个较实用的补救办法是在切换前调用session.Clear()。Clear()会把一级缓存清空让新旧租户的实例在缓存层面不产生冲突。但它的副作用是当前会话中所有未保存的变更也全部丢掉所以不到万不得已不要这么干。我在一个报表导出功能里这么做过一次因为那个功能只读、不写数据安全性高勉强能接受。但凡有写操作一律按一租户一 Session处理。4.5 与命名过滤器filter的结合使用如果你觉得只靠 entity-name 做租户隔离条件还不够细比如同一张租户表里还要按部门过滤那么 entity-name 和 NHibernate 的 filter 机制可以叠加。例如在 B 租户映射上再定义一个 filterclass nameMyApp.Domain.Ticket, MyApp entity-nameTicket_TenantB tableTicket_TenantB filter namedepartmentFilter condition:deptId DepartmentId/ /class启用 filter 的方式和普通场景一样session.EnableFilter(departmentFilter) .SetParameter(deptId, currentDeptId);这套组合的好处是entity-name 解决了物理隔离在哪张表的粗粒度问题filter 解决了表内按行过滤的细粒度问题两者互不冲突逻辑非常清晰。5. 方法论扩展与我的个人体会5.1 从 entity-name 到动态映射的思路升级做完了这个项目我对 entity-name 的理解从一个给实体起别名的语法糖升级成了动态映射的手段。NHibernate 的映射元数据本身是可以在运行时被查询的entity-name 让开发者在不改代码、不重建 SessionFactory 的前提下让同一个类拥有不同的持久化视角。这带来了一个很实用的架构模式配置驱动的多租户支持。你可以把每个租户的实体名、表名、缓存 Region、甚至增删改查的策略都放到外部配置文件或数据库里。业务代码只写一套运行时通过一个解析服务把租户标识转换成实体名。我在项目里做了一个简单的映射注册服务核心简化成一段工厂方法public static class TenantSessionFactory { private static ISessionFactory _factory; public static void Initialize(params Assembly[] mappingAssemblies) { var cfg new NHibernate.Cfg.Configuration(); foreach (var asm in mappingAssemblies) { cfg.AddAssembly(asm); } _factory cfg.BuildSessionFactory(); } public static ISession OpenSessionForTenant(string tenantCode) { var session _factory.OpenSession(); session.Filter new TenantFilter(tenantCode); // 自定义实现 return session; } }这种封装用起来确实有一次配置、多处复用的快感。5.2 什么时候该放弃 entity-name尽管 entity-name 很强大但它不是万能的。我事后总结了一下在以下几种场景里它并不合适每个租户的表结构差异很大。entity-name 只能帮你把映射指向不同表但不同表的字段集合如果差异太多重复书写的映射配置会非常臃肿还不如各建各的 SessionFactory。需要租户级别完全隔离的连接池、缓存策略和部署单元。当隔离粒度上升到基础设施层面那就不该在一个进程里共享 SessionFactory 了。历史项目里大量使用了泛型仓储、泛型泛型 API。改造实体名 API 的工作量远比想象中大如果项目已经稳定运行仅仅为了引入多租户而大改数据访问层收益未必大于风险。我自己更推荐的做法是先分析清楚租户隔离的粒度再决定用 entity-name 还是多 SessionFactory或者两者混用。比如工单主数据隔离用 entity-name财务上报数据因为表结构差异大单独走一个 SessionFactory完全可行。5.3 踩过几次坑之后的几点体会最后再聊点私货。第一一个 Session 绑定一个实体名上下文。这是整个多租户 entity-name 实践里最核心的一条纪律。违反它你会在缓存异常、关联错乱、事务边界混乱的泥潭里挣扎。第二命名规范要提前定死。Ticket_TenantA、Operator_TenantA这种格式看着朴素但团队协作时一眼就能识别归属为排查问题节省了大量时间。如果你让每个开发者自由发挥实体名风格那将是灾难。第三不要迷信所有缓存的 Region 都要按实体名拆分。有些查询是全局性的比如统计所有租户的总工单数这类聚合查询用一张汇总表做更合适而不是强行拿两个实体名的缓存数据拼。别为了租户隔离而把所有共享查询也隔离了那是矫枉过正。第四把实体名的解析收敛到一个地方。任何时候业务代码里都不应该直接写Ticket_TenantA字符串。它应该来自租户解析服务的返回值。这样万一以后租户调整或者从表隔离变为字段隔离改动面可以尽量小。这个特性后续还可以继续扩展的方向也不少。比如把实体名进一步推进到按环境切表测试环境用Ticket_Test、生产环境用Ticket_Prod同一套代码上线却可以连不同的表或者把 entity-name 和 NHibernate 的IMutableSession结合在会话内动态决定持久化策略。无论往哪个方向做理解了实体名其实是在类和数据库对象之间增加了一个可编程的中间层思路就打开了。以上是我在模拟项目X里完整走了一遍 entity-name 之后的全部手记。如果你正在处理多租户、动态映射、或者单纯想给老项目加一套逻辑隔离机制不妨在你的项目里先做一个最小的实体名映射 Demo跑一遍保存、查询、关联加载三个动作体感会非常直观。