NocoBase 关联数据操作指南:RelationRepository 原理与实战
2026/9/14 13:25:36 网站建设 项目流程

NocoBase 关联数据操作指南:RelationRepository 原理与实战

【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase

RelationRepository是 NocoBase 数据库层用于操作关联数据的关系型 Repository 抽象,它允许开发者在不预先加载(include)关联对象的情况下,直接对BelongsToHasOneHasManyBelongsToMany四种关联进行增删改查。本文以官方 API 文档为主线,结合 relation-repository 源码 展开讲解构造函数、基类属性、四种派生 Repository 的全部类方法、中间表与事务机制,帮助你在业务代码中正确、高效地操作关联数据。

什么是 RelationRepository

在 NocoBase 中,普通Repository负责对某个 Collection 的数据进行 CRUD;而RelationRepository关系类型Repository对象,其核心价值在于:在不加载关联的情况下即可对关联数据进行操作。你只需持有源记录(source)的主键值(sourceKeyValue)与关联名称(association),就能通过它读写目标(target)一侧的关联数据。

基于RelationRepository,每种关联都派生出了对应的实现:

  • HasOneRepository—— 一对一(源侧持有外键)
  • HasManyRepository—— 一对多
  • BelongsToRepository—— 多对一(目标侧持有外键)
  • BelongsToManyRepository—— 多对多(通过中间表)

从源码结构看,它们分别继承自SingleRelationRepositoryMultipleRelationRepository两个抽象基类,再统一继承RelationRepository(见 single-relation-repository.ts 与 multiple-relation-repository.ts)。

构造函数与核心参数

签名

constructor(sourceCollection: Collection, association: string, sourceKeyValue: string | number)

参数

参数名类型默认值描述
sourceCollectionCollection-关联中的参照关系(referencing relation)对应的 Collection
associationstring-关联名称
sourceKeyValuestring \| number-参照关系中对应的 key 值

在 relation-repository.ts 的构造实现中,构造函数会依次完成以下初始化:

  1. sourceCollection.context.database取出数据库实例并赋值给db
  2. 调用setSourceKeyValue(sourceKeyValue)处理源 key 值;
  3. 通过this.sourceCollection.model.associations[association]取得 sequelize 的association对象;
  4. 通过this.sourceCollection.getField(association)取得对应的关联字段associationField
  5. association.target得到目标模型,再经database.modelCollection.get(targetModel)解析出targetCollection

其中setSourceKeyValue值得注意:当传入的sourceKeyValue是字符串时,会先尝试用decodeMultiTargetKey对其做decodeURIComponent+JSON.parse解码(见 relation-repository.ts)。这意味着当源表使用复合主键时,可以把多个 key 编码成一个 JSON 字符串传入,配合isMultiTargetKey()判断是否为复合主键场景——这是RelationRepository支持复合主键源记录的基础。

两种获取方式

// 方式一:通过源 Repository 的 relation() 获取(推荐) const userProfileRepository = User.repository.relation('profile').of(user.get('id')); // 方式二:直接初始化 new HasOneRepository(User, 'profile', user.get('id'));

基类属性

RelationRepository暴露了以下基类属性,可在派生类与业务代码中直接访问:

属性类型说明
dbDatabase数据库对象
sourceCollectionCollection关联中的参照关系(referencing relation)对应的 Collection
targetCollectionCollection关联中被参照关系(referenced relation)对应的 Collection
associationAssociationsequelize 中与当前关联对应的 association 对象
associationFieldRelationFieldCollection 中与当前关联对应的字段
sourceKeyValueTargetKey参照关系中对应的 key 值

此外,源码中还提供了几个便捷成员与方法:targetModel(关联目标模型)、sourceInstance(缓存的源记录实例)、collectiongetter(等价于db.getCollection(targetModel.name))。getSourceModel()(relation-repository.ts)会按sourceKeyValue从源 Collection 查出源记录并缓存,供createremoveset等操作复用;若源记录不存在,多数查询型操作会返回null

基类通用方法

除派生类各自实现的方法外,基类RelationRepository还提供了一批可被所有关系 Repository 复用的方法:

  • create(options?: CreateOptions):创建关联对象。支持values为数组时批量创建(Promise.all并发);创建前会通过UpdateGuard.fromOptions做字段白名单/黑名单校验,并通过collection.validate校验数据,创建完成后触发{collection}.afterCreateWithAssociations{collection}.afterSaveWithAssociations事件(见 relation-repository.ts)。
  • firstOrCreate(options):按filterKeysvalues中提取过滤条件,先findOne,命中则直接返回,否则create
  • updateOrCreate(options):同上,但命中时改为update(以filterByTk定位目标记录)。
  • chunk(options):按chunkSize分块遍历关联数据,每块调用callback(rows, options),适合大批量关联数据的批处理场景。
  • convertTk/convertTks:归一化tk参数,支持将逗号分隔的字符串转换为数组。

单值关联:HasOneRepository 与 BelongsToRepository

HasOneRepositoryHasOne类型的关联 Repository(一对一,外键在目标表),BelongsToRepository处理BelongsTo关系(外键在源表),两者的接口与行为完全一致(belongs-to-repository.md 明确指出其接口与HasOneRepository一致)。它们在源码中共同继承自SingleRelationRepository

示例:初始化

const User = db.collection({ name: 'users', fields: [ { type: 'hasOne', name: 'profile' }, { type: 'string', name: 'name' }, ], }); const Profile = db.collection({ name: 'profiles', fields: [{ type: 'string', name: 'avatar' }], }); const user = await User.repository.create({ values: { name: 'u1' }, }); // 获取到关联 Repository const UserProfileRepository = User.repository.relation('profile').of(user.get('id')); // 也可直接初始化 new HasOneRepository(User, 'profile', user.get('id'));

find()

查找关联对象,不存在时返回null

签名

async find(options?: SingleRelationFindOption): Promise<Model<any> | null>
interface SingleRelationFindOption extends Transactionable { fields?: Fields; except?: Except; appends?: Appends; filter?: Filter; }

查询参数与Repository.find()一致。从源码看(single-relation-repository.ts),其实现会先取得源记录,再由filterOptions(sourceModel)构造外键过滤条件,与用户传入的filter$and合并后交给目标 Collection 的repository.findOne执行,因此天然限定在当前关联范围内。

const profile = await UserProfileRepository.find(); // 关联对象不存在时,返回 null

create()

创建关联对象,自动写入外键。

签名

async create(options?: CreateOptions): Promise<Model>

CreateOptions类型定义见 create-options.md:

interface CreateOptions extends SequelizeCreateOptions { values?: Values; whitelist?: WhiteList; // 白名单:仅名单内字段可写入 blacklist?: BlackList; // 黑名单:名单内字段不允许写入 updateAssociationValues?: AssociationKeysToBeUpdate; context?: any; }
const profile = await UserProfileRepository.create({ values: { avatar: 'avatar1' }, }); console.log(profile.toJSON()); /* { id: 1, avatar: 'avatar1', userId: 1, updatedAt: 2022-09-24T13:59:40.025Z, createdAt: 2022-09-24T13:59:40.025Z } */

注意输出中的userId: 1——关联创建时会自动补齐源记录的外键。

update()

更新关联对象,若关联对象不存在会抛出The record does not exist

签名

async update(options: UpdateOptions): Promise<Model>

UpdateOptions类型定义见 update-options.md,其中filterByTkfilter至少要传其一:

interface UpdateOptions extends Omit<SequelizeUpdateOptions, 'where'> { values: Values; filter?: Filter; filterByTk?: TargetKey; whitelist?: WhiteList; blacklist?: BlackList; updateAssociationValues?: AssociationKeysToBeUpdate; context?: any; }
const profile = await UserProfileRepository.update({ values: { avatar: 'avatar2' }, }); profile.get('avatar'); // 'avatar2'

remove()

仅解除关联关系,不删除关联对象。实现上调用 sequelize 单值关联的set(null)accessor(见 single-relation-repository.ts),将外键置空。

签名

async remove(options?: Transactionable): Promise<void>
await UserProfileRepository.remove(); (await UserProfileRepository.find()) == null; // true (await Profile.repository.count()) === 1; // true

destroy()

删除关联对象本身(连带解除关联)。

签名

async destroy(options?: Transactionable): Promise<Boolean>
await UserProfileRepository.destroy(); (await UserProfileRepository.find()) == null; // true (await Profile.repository.count()) === 0; // true

removedestroy对比:前者外键置空、目标记录保留;后者目标记录被物理删除。

set()

将关联设置为指定的目标记录。

签名

async set(options: TargetKey | SetOption): Promise<void>
interface SetOption extends Transactionable { tk?: TargetKey; }
const newProfile = await Profile.repository.create({ values: { avatar: 'avatar2' }, }); await UserProfileRepository.set(newProfile.get('id')); (await UserProfileRepository.find()).get('id') === newProfile.get('id'); // true

多值关联:HasManyRepository

HasManyRepository用于处理HasMany(一对多)关系,其查询类方法返回记录数组,关系维护类方法(add/remove/set)接收单个或多个 targetKey。

查询类方法

find()—— 查找关联对象列表:

async find(options?: FindOptions): Promise<M[]>

查询参数与Repository.find()一致。

findOne()—— 仅返回一条记录:

async findOne(options?: FindOneOptions): Promise<M>

count()—— 返回符合查询条件的记录数:

async count(options?: CountOptions)
interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable { filter?: Filter; }

findAndCount()—— 同时返回数据集与总数:

async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]>
type FindAndCountOptions = CommonFindOptions;

从 multiple-relation-repository.ts 的实现看,findAndCount内部是分别调用findcount(共享同一事务),返回值形如[rows, total],非常契合分页场景。

写入类方法

create()update():创建/更新关联对象,options类型与上文CreateOptions/UpdateOptions一致。

destroy()—— 删除符合条件的关联对象:

async destroy(options?: TK | DestroyOptions): Promise<M>

add()—— 添加对象关联关系(不创建目标记录,仅建立关联):

async add(options: TargetKey | TargetKey[] | AssociatedOptions)
interface AssociatedOptions extends Transactionable { tk?: TargetKey | TargetKey[]; }

tk是关联对象的 targetKey 值,可以是单个值,也可以是数组。

remove()—— 移除与给定对象之间的关联关系,参数同add()

set()—— 设置当前关系的关联对象(整体替换:先移除旧的,再添加新的),参数同add()

多对多:BelongsToManyRepository

BelongsToManyRepository用于处理BelongsToMany(多对多)关系。不同于其他关系类型,多对多关系需要通过中间表(through)来记录;在 NocoBase 中定义关联关系时,既可以自动创建中间表,也可以明确指定中间表。中间表还可携带额外字段,通过add/set一并写入。

查询类方法

find()findOne()count()findAndCount()HasManyRepository完全一致(签名与类型定义相同,此处不再重复)。

值得一提的是find的底层实现(multiple-relation-repository.ts):它会基于association.otherKeytargetKey动态构造一个指向中间表的HasOnepivot 关联(as: '_pivot_'),并通过include的方式按源记录过滤,从而在目标 Repository 上完成带中间表感知的关联查询;若中间表定义了scope,也会被归一化后并入查询。

写入类方法

create()/update()/destroy():签名与HasManyRepository对应方法一致(destroy返回Promise<Boolean>)。

add()—— 添加新的关联对象,支持同时写入中间表字段

async add( options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions ): Promise<void>
type PrimaryKeyWithThroughValues = [TargetKey, Values]; interface AssociatedOptions extends Transactionable { tk?: | TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[]; }

可以直接传入关联对象的targetKey,也可以将targetKey与中间表的字段值以元组[TargetKey, Values]的形式一并传入。

示例

const t1 = await Tag.repository.create({ values: { name: 't1' }, }); const t2 = await Tag.repository.create({ values: { name: 't2' }, }); const p1 = await Post.repository.create({ values: { title: 'p1' }, }); const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id); // 传入 targetKey await PostTagRepository.add([t1.id, t2.id]); // 传入中间表字段 await PostTagRepository.add([ [t1.id, { tagged_at: '123' }], [t2.id, { tagged_at: '456' }], ]);

set()—— 设置关联对象(整体替换),参数同add(),同样支持[TargetKey, Values]元组。

remove()—— 移除与给定对象之间的关联关系(仅删除中间表记录,不删除目标记录):

async remove(options: TargetKey | TargetKey[] | AssociatedOptions)
interface AssociatedOptions extends Transactionable { tk?: TargetKey | TargetKey[]; }

toggle()—— 切换关联对象:自动判断关联关系是否已存在,存在则移除,不存在则添加。适用于「收藏/取消收藏」「关注/取关」这类业务场景。

async toggle(options: TargetKey | { tk?: TargetKey; transaction?: Transaction }): Promise<void>
// 首次调用:添加关联 await PostTagRepository.toggle(t1.id); // 再次调用:移除关联 await PostTagRepository.toggle(t1.id);

事务机制

RelationRepository的多数写入方法(createupdateremovesetdestroyfirstOrCreateupdateOrCreate)都带有@transaction()装饰器。从 relation-repository.ts 可以看到其事务工厂实现:

export const transaction = transactionWrapperBuilder(function () { return this.sourceCollection.model.sequelize.transaction(); });

行为规则为:如果没有传入事务参数,方法会自动创建一个内部事务;若调用方通过options.transaction传入了外部事务,则复用该事务(见getTransaction,relation-repository.ts)。这保证了关联操作与业务主流程的一致性:例如add批量建立关联、toggle的判断与写入,都在同一事务内原子完成。

源码级原理小结

关注点实现位置要点
关系基类与通用能力relation-repository.ts构造函数初始化、复合主键解码、firstOrCreate/updateOrCreate/chunk、事务装饰器
单值关联(HasOne/BelongsTo)single-relation-repository.tsfind$and合并外键过滤;remove等价于set(null)update目标不存在时抛错
多值关联(HasMany)multiple-relation-repository.tsfindAndCount=find+count共享事务;count排除BelongsToArray关联
多对多(BelongsToMany)同上的 pivot 关联构造find动态构造_pivot_HasOne 关联,感知中间表 scope 与额外字段

对应的派生实现文件分别为 hasone-repository.ts、hasmany-repository.ts、belongs-to-repository.ts、belongs-to-many-repository.ts,以及统一的关系参数类型定义 types.ts。

使用建议

  • 优先通过repository.relation(name).of(key)获取关系 Repository,它替你完成了构造与参数归一化,代码更可读;直接new适合在插件内部需要显式控制场景时使用。
  • 区分removedestroy:解除关系用remove(数据保留),删除目标记录用destroy;对BelongsToMany,两者分别对应删除中间表记录与删除目标记录。
  • 多对多带业务属性的中间表:利用add/set[TargetKey, Values]元组一次性写入中间表字段,避免二次更新。
  • 复合主键源记录sourceKeyValue可传入 JSON 字符串,由decodeMultiTargetKey自动解码,配合isMultiTargetKey判断分支。
  • 批量数据处理:大批量遍历关联数据时使用基类的chunk方法,按chunkSize分批处理并自动维护offset
  • 事务边界:多步关联写入建议显式传入同一个transaction,让所有操作在同一事务内提交或回滚。

【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询