PAYATHON 2026

Access 2007 中用单个 Notes 表关联多个表

支付阿杰

结论

不要采用 TableName + ID 形式的”多态外键”,也不必增加 NotesLookup。Access 无法为这种关联实施真正的参照完整性。表名拼错,或者目标记录被删除后留下孤立备注,都只能通过 VBA 或查询自行处理。

对于目前固定的四类记录,可以只使用一个 Notes 表,并设置四个允许为空的外键:

Notes
-----
NoteID
NoteText
InjuryID
TreatmentID
InvoiceID
PaymentID

每条备注必须且只能填写一个外键。这样既能保留单一备注表,也可以继续使用 Access 的关系、级联规则和主窗体/子窗体自动保存机制。

推荐的表结构

假设四个主表的主键都是 AutoNumber,Notes 中对应的外键应使用 Long Integer,不能使用 AutoNumber。

Injury
-------
InjuryID       AutoNumber, Primary Key
...

Treatment
---------
TreatmentID    AutoNumber, Primary Key
...

Invoice
-------
InvoiceID      AutoNumber, Primary Key
...

Payment
-------
PaymentID      AutoNumber, Primary Key
...

Notes
-----
NoteID         AutoNumber, Primary Key
NoteText       Long Text
CreatedAt      Date/Time
InjuryID       Long Integer, Null allowed
TreatmentID    Long Integer, Null allowed
InvoiceID      Long Integer, Null allowed
PaymentID      Long Integer, Null allowed

备注内容字段建议命名为 NoteText,不要使用含义过于宽泛的 Note。这样查询和 VBA 表达式也更容易读懂。

在 Access 的”数据库工具 → 关系”中建立以下一对多关系:

Injury.InjuryID       1 ─── ∞ Notes.InjuryID
Treatment.TreatmentID 1 ─── ∞ Notes.TreatmentID
Invoice.InvoiceID     1 ─── ∞ Notes.InvoiceID
Payment.PaymentID     1 ─── ∞ Notes.PaymentID

这些关系都可以启用”实施参照完整性”。“级联删除相关记录”是否启用,则取决于具体的业务规则。如果删除发票时必须一并删除相关备注,可以启用级联删除。如果备注需要保留以供审计,就不应启用,而应禁止删除主记录或采用逻辑删除。

限制每条备注只能关联一种记录

四个外键都可以为空,但一条有效备注必须恰好有一个外键不为空。可以在 Notes 表的表级”有效性规则”中填写:

IIf([InjuryID] Is Null,0,1)
+ IIf([TreatmentID] Is Null,0,1)
+ IIf([InvoiceID] Is Null,0,1)
+ IIf([PaymentID] Is Null,0,1)
= 1

对应的”有效性文本”可以设为:

每条备注必须且只能关联 Injury、Treatment、Invoice 或 Payment 中的一条记录。

这条规则会阻止两种错误:

  • 四个外键全部为空;
  • 同一条备注填写了两个或更多外键。

如果业务上允许先保存草稿备注,之后再建立关联,可以把规则改为 <= 1。这样做会允许暂时没有归属的备注存在。

在窗体中自动保存关联

Access 的主窗体/子窗体机制可以自动填写外键,不需要编写 VBA。

以 Injury 窗体为例:

  1. 将主窗体的记录源设为 Injury。
  2. 创建一个以 Notes 为记录源的连续窗体或数据表窗体。
  3. 把这个备注窗体作为子窗体放入 Injury 主窗体。
  4. 设置子窗体控件的关联字段:
Link Master Fields: InjuryID
Link Child Fields:  InjuryID

当用户在某条 Injury 记录下新增备注时,Access 会自动把当前 InjuryID 写入 Notes.InjuryID。

其余三个窗体采用相同的设置:

Treatment form:
Link Master Fields: TreatmentID
Link Child Fields:  TreatmentID

Invoice form:
Link Master Fields: InvoiceID
Link Child Fields:  InvoiceID

Payment form:
Link Master Fields: PaymentID
Link Child Fields:  PaymentID

可以为四种场景分别创建备注子窗体,也可以复用同一个子窗体,再由外层窗体设置不同的链接字段。无论采用哪种方式,都应隐藏四个外键控件,避免用户手工修改关联。

查询全部备注

如果需要生成跨类型的统一备注列表,可以使用 UNION ALL 查询。下面的 RecordLabel 只是示例字段,需要替换成各表中实际用于显示记录的字段:

SELECT
    N.NoteID,
    N.NoteText,
    N.CreatedAt,
    'Injury' AS RecordType,
    N.InjuryID AS RecordID,
    I.InjuryNumber AS RecordLabel
FROM Notes AS N
INNER JOIN Injury AS I
    ON N.InjuryID = I.InjuryID

UNION ALL

SELECT
    N.NoteID,
    N.NoteText,
    N.CreatedAt,
    'Treatment' AS RecordType,
    N.TreatmentID AS RecordID,
    T.TreatmentNumber AS RecordLabel
FROM Notes AS N
INNER JOIN Treatment AS T
    ON N.TreatmentID = T.TreatmentID

UNION ALL

SELECT
    N.NoteID,
    N.NoteText,
    N.CreatedAt,
    'Invoice' AS RecordType,
    N.InvoiceID AS RecordID,
    I.InvoiceNumber AS RecordLabel
FROM Notes AS N
INNER JOIN Invoice AS I
    ON N.InvoiceID = I.InvoiceID

UNION ALL

SELECT
    N.NoteID,
    N.NoteText,
    N.CreatedAt,
    'Payment' AS RecordType,
    N.PaymentID AS RecordID,
    P.PaymentNumber AS RecordLabel
FROM Notes AS N
INNER JOIN Payment AS P
    ON N.PaymentID = P.PaymentID;

表级规则保证每条备注只有一个外键有值,因此一条备注只会出现在一个查询分支中。

为什么不推荐 TableName + ID

下面这种结构看起来很灵活:

Notes
-----
NoteID
TableName
RecordID
NoteText

但 RecordID 可能指向四张不同的表,Access 无法为它建立普通外键。数据库也就无法自动保证:

  • TableName 是合法的表名;
  • RecordID 存在于指定的表中;
  • 删除主记录时能同步处理备注;
  • 表名变更后能自动更新备注;
  • RecordID 的类型与目标主键一致。

查询备注时,还必须根据 TableName 动态选择目标表。普通关系查询、窗体链接字段和级联操作都无法直接完成这种解引用。

把这些字段移到 NotesLookup 也解决不了问题,只是把无法验证的多态引用放到了另一张表中。只有在同一条备注确实需要同时关联多个业务记录时,中间表才有意义。即使使用中间表,TableName + ID 仍然无法获得数据库级的外键保护。

可选的超类型设计

如果 Injury、Treatment、Invoice 和 Payment 在业务上都属于同一种实体,例如 Record 或 CaseItem,可以引入一个公共父表:

Record
------
RecordID
RecordType

Injury
------
RecordID    Primary Key, Foreign Key
...

Treatment
---------
RecordID    Primary Key, Foreign Key
...

Notes
-----
NoteID
RecordID    Foreign Key
NoteText

从关系模型看,这种设计更规整,因为 Notes.RecordID 始终引用同一张父表。但在创建任何 Injury、Treatment、Invoice 或 Payment 记录时,都要先创建对应的 Record,再创建子类型记录。Access 窗体也需要同时维护父表和子表,设计复杂度通常高于使用四个可空外键,部分新增流程还可能需要宏或 VBA。

如果这四张表只是现有系统中固定的四类独立业务对象,并没有共享父实体的实际需求,就不值得只为备注功能引入超类型。

在不额外编写 VBA、同时又要保留一个 Notes 表的情况下,使用四个可空外键,并通过表级有效性规则保证”恰好一个非空”,是 Access 2007 中最直接、可靠的方案。

备注:内容仅供参考。