Sequelize关联查询中attributes覆盖defaultScope的方案与bug问询
复现场景
初始模型定义
const Bar = sequelize.define( 'Bar', { id: DataTypes.STRING, name: DataTypes.STRING, }, { defaultScope: { attributes: ['id'] }, scopes: { withName: { attributes: { include: ['name'] } } } } );
正常生效的关联查询
不指定自定义attributes的关联场景下,defaultScope正常生效,仅返回预设的id字段:
Foo.findAll({ include: [{ model: Bar, }], }) /* 返回结果 { ... Bar:[ { id: 'qux' } ] } */
预期不符的异常场景
直接在关联配置中追加attributes配置希望额外返回name字段时,defaultScope预设的id字段会直接丢失:
Foo.findAll({ include: [{ model: Bar, attributes: { include: ['name'] } }], }) /* 返回结果 { ... Bar:[ { name: 'quux' } ] } */
现有解决方案的痛点
- 为每个需要额外返回的字段单独创建自定义scope:每新增一个返回字段,就要在模型定义中手动新增一个scope条目,长期维护成本极高
- 手动把defaultScope字段、需要追加的字段全部写入关联的attributes配置:完全失去了使用defaultScope统一管理核心返回字段的意义
核心问题解答
关于关联配置attributes覆盖defaultScope的行为本质
关联查询中显式传入attributes会直接覆盖defaultScope字段列表的行为,是Sequelize v5.x版本的内置解析逻辑,并非已知bug。
v5版本的参数优先级规则为:查询方法、关联配置中显式传入的attributes参数,会完全替换掉所有scope(包括defaultScope)中定义的attributes配置,不会自动做字段合并。因此在关联配置中传入attributes: { include: ['name'] }时,程序不会先加载defaultScope预设的['id']再做字段合并,而是直接将整个attributes配置替换为当前传入的内容,最终仅返回name字段。
Sequelize v6.x版本已经对该逻辑做了优化:当查询/关联配置中传入的attributes为包含include/exclude结构的对象时,会自动和当前生效scope(包括defaultScope)的字段列表做合并,不会直接替换原有配置。v5版本中出现的id丢失问题,在v6.x版本中通过原有写法即可直接解决,无需额外配置。
无需单独定义scope的实现方法(适配Sequelize v5.x)
在v5.21.4版本中,可以直接动态读取模型defaultScope中定义的字段列表,在关联配置中做字段拼接,不需要为单个字段单独创建scope:
Foo.findAll({ include: [{ model: Bar, // 自动合并defaultScope预设的核心字段 + 当前查询需要额外返回的字段 attributes: [ ...Bar.options.defaultScope.attributes, 'name' ] }] })
该方法的核心优势:
- 核心返回字段仅在模型的defaultScope中维护一次,不存在重复定义
- 每次查询时可灵活追加任意需要返回的字段,不需要提前在模型中注册对应scope
- 完全保留defaultScope的配置收益,不会出现字段列表硬编码的问题
内容的提问来源于stack exchange,提问作者Fre Timmerman

