LiteOrm在线文档

关联查询(TableJoin / ForeignType / AutoExpand)

本文档介绍 LiteOrm 的关联查询能力。

1. 概念总览


2. 使用示例

2.1 最小可用闭环

下面这个例子适合第一次接触 LiteOrm 关联查询时先跑通:

[Table("Users")]
public class User
{
    [Column("Id", IsPrimaryKey = true, IsIdentity = true)]
    public int Id { get; set; }

    [Column("UserName")]
    public string? UserName { get; set; }
}

[Table("Orders")]
public class Order
{
    [Column("Id", IsPrimaryKey = true, IsIdentity = true)]
    public int Id { get; set; }

    [Column("UserId")]
    [ForeignType(typeof(User))]
    public int UserId { get; set; }
}

public class OrderView : Order
{
    [ForeignColumn(typeof(User), Property = nameof(User.UserName))]
    public string? UserName { get; set; }
}
var orders = await orderService.SearchAsync<OrderView>();

如果 OrderView.UserName 能正确取到值,说明最基础的 ForeignType + ForeignColumn 关联链已经打通。

2.2 ForeignType(属性级)

[Table("Orders")]
public class Order
{
    [Column("Id", IsPrimaryKey = true, IsIdentity = true)]
    public int Id { get; set; }

    [Column("UserId")]
    [ForeignType(typeof(User), Alias = "U", JoinType = TableJoinType.Left, AutoExpand = false)]
    public int UserId { get; set; }

    [Column("Amount")]
    public decimal Amount { get; set; }

2.2.1 一个列上声明多个 ForeignType

现在同一个列可以重复声明多个 ForeignType。适合“底层还是单列外键,但需要暴露多条可读关联路径”的场景。

[Table("Documents")]
public class Document
{
    [Column("OwnerId")]
    [ForeignType(typeof(User), Alias = "Owner")]
    [ForeignType(typeof(Department), Alias = "OwnerDept")]
    public int OwnerId { get; set; }
}

public class DocumentView : Document
{
    [ForeignColumn("Owner", Property = nameof(User.UserName))]
    public string? OwnerName { get; set; }

    [ForeignColumn("OwnerDept", Property = nameof(Department.Name))]
    public string? OwnerDeptName { get; set; }
}

2.3 TableJoin(类级)

[TableJoin(typeof(Department), "ParentId", AliasName = "Parent", JoinType = TableJoinType.Left)]
[TableJoin(typeof(Department), "DeptId", AliasName = "Dept", JoinType = TableJoinType.Left)]
public class User { /* ... */ }

public class OrderView : Order
{
    [ForeignColumn(typeof(User), Property = "UserName")]
    public string? UserName { get; set; }

    [ForeignColumn("Dept", Property = "Name")]
    public string? DeptName { get; set; } // 使用 TableJoin 的 Alias 引用
}
[TableJoin(typeof(OrderItem), "OrderId,LineNo", AliasName = "Item")]
public class Shipment
{
    [Column("OrderId")]
    public long OrderId { get; set; }

    [Column("LineNo")]
    public int LineNo { get; set; }
}

这类模型里,Shipment.OrderId + Shipment.LineNo 会按顺序关联到 OrderItem 的联合主键。

2.4 多级关联与 AutoExpand

// SalesRecord 示例:SalesUserId 关联 User,并自动展开 User 的关联(如 Department)
[Column("SalesUserId")]
[ForeignType(typeof(User), AutoExpand = true)]
public int SalesUserId { get; set; }

public class SalesRecordView : SalesRecord
{
    [ForeignColumn(typeof(User))]
    public string? UserName { get; set; }

    [ForeignColumn(typeof(Department), Property = nameof(Department.Name))]
    public string? DepartmentName { get; set; }
}

// 查询 SalesRecordView 时,LiteOrm 可以继续沿着 User 已定义好的关联路径解析 DepartmentName。

2.4.1 AutoExpand 开关对比

场景 AutoExpand = false AutoExpand = true
只需要一级外表字段 推荐 也可用,但通常没有必要
需要跨二级关联读取字段 需要手动声明更多连接 推荐
大表、复杂视图、性能敏感 更稳妥 需谨慎评估
想减少视图声明复杂度 一般 更方便

2.5 级联示例

LiteOrm.Demo\Models\SalesRecord.cs 给出了一个很实用的二级关联展开模型:

[Table("Sales_{0}")]
public class SalesRecord : ObjectBase, IArged
{
    [Column("SalesUserId")]
    [ForeignType(typeof(User), AutoExpand = true)]
    public int SalesUserId { get; set; }
}

public class SalesRecordView : SalesRecord
{
    [ForeignColumn(typeof(User))]
    public string? UserName { get; set; }

    [ForeignColumn(typeof(Department), Property = nameof(Department.Name))]
    public string? DepartmentName { get; set; }
}

这里的关键点是:

如果没有开启 AutoExpandDepartmentName 这类二级字段通常需要额外声明连接路径。
这也是 AutoExpand 最常见、也最值得使用的场景:补足多级关联的可解析路径。 更多示例请参考代码中的 Demo(LiteOrm.Demo.Models)以及单元测试中的 TableJoin/AutoExpand 相关测试用例。

2.6 多级关联示例

演示“部门 + 上级部门”两级关联:

[Table("Users")]
[TableJoin("Dept", typeof(Department), nameof(Department.ParentId), AliasName = "Parent")]
public class User
{
    [Column("DeptId")]
    [ForeignType(typeof(Department), Alias = "Dept")]
    public int? DeptId { get; set; }
}

public class UserView : User
{
    [ForeignColumn("Dept", Property = "Name")]
    public string? DeptName { get; set; }

    [ForeignColumn("Parent", Property = "Name")]
    public string? ParentDeptName { get; set; }
}

这类写法适合“用户 → 部门 → 上级部门”这种稳定的多级读取场景。

2.7 查询示例

验证多级关联字段是否可用于筛选:

var usersByDept = await viewService.SearchAsync(u => u.DeptName == "Sub Dept");
var usersByParentDept = await viewService.SearchAsync(u => u.ParentDeptName == "Root Dept");

var combinedUsers = await viewService.SearchAsync(
    u => u.DeptName == "Sub Dept" && u.ParentDeptName == "Root Dept"
);

2.7.1 关联字段排序与分页

LiteOrm.Tests\ServiceTests.cs 还验证了关联字段可以直接参与排序与分页:

using static LiteOrm.Common.Expr;
var expr1 = From<TestUserView>()
    .Where<TestUserView>(u => u.DeptName != null)
    .OrderBy((nameof(TestUserView.DeptName), true))
    .OrderBy((nameof(TestUser.Age), false))
    .Section(0, 3);

var users1 = await viewService.SearchAsync(expr1);

以及更深一层的父部门字段:

using static LiteOrm.Common.Expr;
var expr2 = From<TestUserView>()
    .Where<TestUserView>(u => u.ParentDeptName == "Parent Dept")
    .OrderBy(nameof(TestUserView.ParentDeptName))
    .OrderBy(nameof(TestUserView.DeptName))
    .OrderBy(nameof(TestUser.Age))
    .Section(0, 5);

var users2 = await viewService.SearchAsync(expr2);

这说明 ForeignColumn 不仅能显示,还能直接参与:


3. ExistsRelated

当你不想在视图模型中显式暴露关联字段,而只是想”按关联表条件过滤主表”时,可以使用 ExistsRelated

3.1 匹配规则

ExistsRelated 在构造关联路径时遵循以下优先级规则:

关联匹配顺序:

  1. 正向关联优先:首先尝试从主表出发的外键关联(如 Order.UserId -> User.Id
  2. 反向关联备选:若主表上没有到目标类型的正向关联,则尝试从目标表反向推断(如 User.DeptId -> Department.Id

这里的“匹配”不是按属性名字符串硬编码比较,而是按模型元数据里的已声明关联来找:

也就是说,ExistsRelated<TestDepartment>(...) 依赖的是 ForeignType / TableJoin 等已经声明好的关联元数据,而不是运行时去猜一个“字段名看起来像外键”的关系。

这也意味着:

多路径时的合并逻辑:

可以把它理解成:

(路径1的键1 = 键1 AND 路径1的键2 = 键2 ...)
OR
(路径2的键1 = 键1 AND 路径2的键2 = 键2 ...)
using static LiteOrm.Common.Expr;
// 查询"拥有名为 ERRev_User1 的用户"的部门
var expr = ExistsRelated<TestUser>(Prop("Name") == "ERRev_User1");
var results = await objectViewDAO.Search(expr).ToListAsync();

即使 TestDepartment 自身没有直接声明到 TestUserForeignType,框架仍可通过 TestUser.DeptId -> TestDepartment.Id 的已知关联关系完成反向推断。

使用建议:

3.2 组合过滤

using static LiteOrm.Common.Expr;
// 1. 正向:按关联部门过滤用户
var expr = ExistsRelated<TestDepartment>(Prop("Name") == "ER_IT");
var users = await objectViewDAO.Search(expr).ToListAsync();

// 2. 取反:排除属于目标部门的用户
var notInIT = await objectViewDAO.Search(
    !ExistsRelated<TestDepartment>(Prop("Name") == "ERNot_IT")
).ToListAsync();

// 3. 组合普通字段条件
var matureItUsers = await objectViewDAO.Search(
    ExistsRelated<TestDepartment>(Prop("Name") == "ERCombo_IT")
    & (Prop("Age") >= 30)
).ToListAsync();

适用建议:


4. API 要点

实现上,LiteOrm 会在元数据阶段合并 ForeignType 与 TableJoin 的信息,生成 JoinedTable / ForeignTable 结构。固定筛选相关的元数据与 SQL 注入细节,见权限过滤与用户范围控制


5. 最佳实践


6. 常见问题


7. 相关链接