Feathers.js / Sequelize -> 服务与两个模型之间的关系
Feathers.js / Sequelize -> Service with relations between two models
我已经 feathers.js 通过 sequelize 与 mysql 一起工作。这很有效,我可以从 tables 收集数据。下一步是在模型中定义 'joins'。
我有一个包含 'status_id' 和 'country_id' 列的 table。这些列引用元数据 table 中的 id。在 SQL 我会是对的:
SELECT status.description, country.description, detail
FROM details
INNER JOIN metadata status
ON (details.status_id = status.id AND status.type = 'status'
INNER JOIN metadata country
ON (details.country_id =country.id AND country.type = 'country')
这种元数据 table 在这种情况下不会很大,因此采用这种方法。它确实提供了我需要的灵活性。
我需要做什么才能在 feathters.js 中完成此操作?
在帮助很多人解决同样的问题后,我了解到解决方案分为两部分:
#1 - 拥抱 ORM
大多数人的问题来自于对sequelize缺乏了解。为了帮助您,您首先需要了解 sequelize associations work and how to perform queries using the "include" option(又名 "eager loading")。我建议将这些链接的所有内容都读几遍,然后再读一遍,以备不时之需;这是 sequelize 学习曲线中最陡峭的部分。如果您从未使用过 ORM,让它为您完成许多繁重的工作!
#2 - 从羽毛挂钩设置续集选项
一旦您理解了 "include" 选项如何与 sequelize 一起使用,您将希望通过羽毛中的 "before" 钩子设置该选项。 Feathers will pass the value of hook.params.sequelize
as the options parameter 所有 sequelize 方法调用。这就是您的钩子的样子:
// GET /my-service?name=John&include=1
function (hook) {
if (hook.params.query.include) {
const AssociatedModel = hook.app.services.fooservice.Model;
hook.params.sequelize = {
include: [{ model: AssociatedModel }]
};
// delete any special query params so they are not used
// in the WHERE clause in the db query.
delete hook.params.query.include;
}
return Promise.resolve(hook);
}
在引擎盖下,羽毛会调用您的模型 find
方法,有点像这样:
// YourModel is a sequelize model
const options = Object.assign({ where: { name: 'John' }}, hook.params.sequelize);
YourModel.findAndCount(options);
值得注意:
旧的 v1.x 羽毛生成器(2017 年 3 月之前)不会生成对 sequelize 友好的代码。这已在新的 v2.x 生成器中修复。如果您在 2017 年 3 月之前对您的项目有相当大的了解,那么不要使用新的生成器。请加入Slack Channel and join the sequelize
room for help. I keep an eye on things there and can help you. If you just started your project and haven't gotten very far, then I highly suggest starting fresh with the new generators. Run this command (and follow these instructions):
$ feathers --version # see what version you are using
$ npm install -g @feathersjs/cli # install latest version of the CLI
好的,我已经对代码做了一些调整。为了让每个人都能读懂内容,我将转向实际的 table 示例。我有一个 table 'sections' 和一个 table 'categories' (这是一个更简单的例子)。该部分有一个带有类别的外键。所以这就是我到目前为止所做的:
类别-model.js
classMethods: {
associate() {
category.hasOne(sequelize.models.sections, {
as: 'category',
foreignKey: 'category_id'
});
},
},
节-model.js
classMethods: {
associate() {
section.belongsTo(sequelize.models.categories, {
allowNull: true
});
},
},
services\index.js
...
app.set('models', sequelize.models);
...
Object.keys(sequelize.models).forEach(function(modelName) {
if ("associate" in sequelize.models[modelName]) {
sequelize.models[modelName].associate();
}
});
在这种情况下,我使用的是 Jetbrains webstorm。所以我做了一个 npm start,我的 table 现在有了正确的外键,所以那部分工作了。此外,数据的显示仍然是正确的。获取这些部分的查询仍在运行。如果我在 index.js 中有不同的编码,那么 npm start 不会失败,但部分查询失败。
接下来是:挂钩。这就是我现在感到困惑的地方。有些网站说它在 'find definition' 中(我们暂时这么称呼它,但这是在我的 vue 组件安装部分)。然后你就得到了解释,显示得很好,包含在那个部分,但它什么都不做,代码仍然运行,但是当我通过 postman 获取这些部分时,我没有看到类别信息。然后我会有例如
serviceSections.find({
include: [{
model: serviceCategories.Model
}],
query: {
$sort: {
section_description_en: 1
}
}
}).then(page => {
page.data.reverse();
this.listSections = page.data;
})
serviceCategories 定义为 "appFeathers.service('categories');"。如前所述,它什么都不做。所以回到我在这里得到的解释,它说'.. from a before hook ..'。我找到了 hooks\index.js 文件,用于类别和服务。但在这里我犯了错误。我首先在类别中进行了此调整,然后在第一个
部分进行了调整
exports.before = {
...
find: [{
function (hook) {
if (hook.params.query.include) {
const AssociatedModel = hook.app.services.category.Model;
hook.params.sequelize = {
include: [{ model: AssociatedModel }]
};
}
return Promise.resolve(hook);
}
}],
...
};
这是代码 500 fn.bind 错误。
只想到 post 进度,这并不意味着我停止寻找最后一步(或缺失的一步)。
我忘了一件事。我检查它是否完成是在chrome中打开F12,进入VUE插件并展开'listSections'这是"serviceSections.find"的结果。我希望在其中看到类别列,但也许这是错误的期望。我也没有在我的调试器
的 select 中看到 'join'
稍后编辑:
好吧,所以我搞砸了。最后我也遇到了这个poston how to retrieve data from many to many relationships。读到这里,我认为调整 "hooks\index.js" 的意图是正确的,但这意味着我的代码不是。所以尝试 post 和上面提供的提示的不同组合,我现在有了这个
节\hooks\index.js
...
exports.before = {
all: [],
find: [
getCategory()
],
get: [
getCategory()
],
create: [],
update: [],
patch: [],
remove: []
};
...
function getCategory() {
return function (hook) {
const category = hook.app.services.categories.Model;
hook.params.sequelize = {
include: [{ model: category }]
};
return Promise.resolve(hook);
};
}
这是在 postman 中使用 GET 工作的,因为将它放在 'before' 部分的 'get' 部分中,它在 VUE 中工作,因为我将函数放在'find' 'before'.
的一部分
多重连接
好的,我需要多个加入 'sections'。我也有地位。这来自我的元数据 table。所以这意味着对元数据模型中的部分进行相同的关联。我是这样做的:
元数据-model.js
classMethods: {
associate() {
metadata.hasOne(sequelize.models.sections, {
as: 'satus',
foreignKey: 'status_id',
targetKey: 'status_id'
});
}, },
这里我开始总是放入foreignKey属性。 targetKey 是另一个表中列的名称。如果你需要改变它很方便。 'as' 属性是一个别名,我喜欢几乎总是使用它,至少在多次使用它时是这样。在部分模型中,我进行了更改。
节-model.js
classMethods: {
associate() {
section.belongsTo(sequelize.models.categories, {
allowNull: false,
as: 'category'
});
section.belongsTo(sequelize.models.metadata, {
allowNull: false,
as: 'status'
});
}, },
为了完成这个,我更改了 hooks 函数。我首先尝试了太函数,但那没有用,所以我将两者合并。
节\hooks\index.js
function getRelatedInfo() {
return function (hook) {
hook.params.sequelize = {
include: [
{
model: hook.app.services.categories.Model,
as: 'category'
},{
model: hook.app.services.metadata.Model,
as: 'status',
where: {
type: 'status'
}
}
]
};
return Promise.resolve(hook);
};
}
如您所见,我更改了函数名称。再次使用别名很重要,它在其他方面不起作用。在元数据部分,我放了一个过滤器。我不想在查找时加载整个 table。好的,我加入了一个 id,所以它确实是一对一的,但是如果由于某种原因元数据条目更改为不同的类型,我仍然在 'status' 上有 selection 并且可以错误行的警告。
最后一部分是您想要内部联接还是左外部联接。我通过钩子部分中的 'required: true' 属性触发了它。
VUE.js部分
最后一部分是将其推送到一个vue组件。我在安装部分这样做。我有这个代码。
const socket = io();
const appFeathers = feathers()
.configure(feathers.socketio(socket))
.configure(feathers.hooks());
const serviceSections = appFeathers.service('sections');
const serviceArticles = appFeathers.service('articles');
const serviceMetadata = appFeathers.service('metadata');
...
mounted() {
serviceArticles.find({
include: [{
model: serviceMetadata.Model,
as: 'country',
query: {
$select: [
'country_icon'
]
}
}],
query: {
$sort: {
article_description_en: 1
},
$select: [
'id',
['article_description_en', 'article_description'],
'article_length',
'article_ascend',
'article_code'
]
}
}).then(page => {
this.listTrails = page.data;
})
}
我在这里所做的是从数组中过滤不需要的列。我也重命名了一些。 '*_en' 是多语言的,所以我需要为它使用一个变量。再次重复包含以从连接中获取相关列。
从 Sequelize 的 v4 开始,classMethods
属性 已被删除 See this reference。这意味着@Edgar 的回答中的示例将不起作用。
associate
方法必须直接添加到模型中,而不是包裹在 classMethods
属性 中。来自 Sequelize 文档:
Previous:
const Model = sequelize.define('Model', {
...
}, {
classMethods: {
associate: function (model) {...}
},
instanceMethods: {
someMethod: function () { ...}
}
});
New:
const Model = sequelize.define('Model', {
...
});
// Class Method
Model.associate = function (models) {
...associate the models
};
// Instance Method
Model.prototype.someMethod = function () {..}
我已经 feathers.js 通过 sequelize 与 mysql 一起工作。这很有效,我可以从 tables 收集数据。下一步是在模型中定义 'joins'。
我有一个包含 'status_id' 和 'country_id' 列的 table。这些列引用元数据 table 中的 id。在 SQL 我会是对的:
SELECT status.description, country.description, detail
FROM details
INNER JOIN metadata status
ON (details.status_id = status.id AND status.type = 'status'
INNER JOIN metadata country
ON (details.country_id =country.id AND country.type = 'country')
这种元数据 table 在这种情况下不会很大,因此采用这种方法。它确实提供了我需要的灵活性。
我需要做什么才能在 feathters.js 中完成此操作?
在帮助很多人解决同样的问题后,我了解到解决方案分为两部分:
#1 - 拥抱 ORM
大多数人的问题来自于对sequelize缺乏了解。为了帮助您,您首先需要了解 sequelize associations work and how to perform queries using the "include" option(又名 "eager loading")。我建议将这些链接的所有内容都读几遍,然后再读一遍,以备不时之需;这是 sequelize 学习曲线中最陡峭的部分。如果您从未使用过 ORM,让它为您完成许多繁重的工作!
#2 - 从羽毛挂钩设置续集选项
一旦您理解了 "include" 选项如何与 sequelize 一起使用,您将希望通过羽毛中的 "before" 钩子设置该选项。 Feathers will pass the value of hook.params.sequelize
as the options parameter 所有 sequelize 方法调用。这就是您的钩子的样子:
// GET /my-service?name=John&include=1
function (hook) {
if (hook.params.query.include) {
const AssociatedModel = hook.app.services.fooservice.Model;
hook.params.sequelize = {
include: [{ model: AssociatedModel }]
};
// delete any special query params so they are not used
// in the WHERE clause in the db query.
delete hook.params.query.include;
}
return Promise.resolve(hook);
}
在引擎盖下,羽毛会调用您的模型 find
方法,有点像这样:
// YourModel is a sequelize model
const options = Object.assign({ where: { name: 'John' }}, hook.params.sequelize);
YourModel.findAndCount(options);
值得注意:
旧的 v1.x 羽毛生成器(2017 年 3 月之前)不会生成对 sequelize 友好的代码。这已在新的 v2.x 生成器中修复。如果您在 2017 年 3 月之前对您的项目有相当大的了解,那么不要使用新的生成器。请加入Slack Channel and join the sequelize
room for help. I keep an eye on things there and can help you. If you just started your project and haven't gotten very far, then I highly suggest starting fresh with the new generators. Run this command (and follow these instructions):
$ feathers --version # see what version you are using
$ npm install -g @feathersjs/cli # install latest version of the CLI
好的,我已经对代码做了一些调整。为了让每个人都能读懂内容,我将转向实际的 table 示例。我有一个 table 'sections' 和一个 table 'categories' (这是一个更简单的例子)。该部分有一个带有类别的外键。所以这就是我到目前为止所做的:
类别-model.js
classMethods: {
associate() {
category.hasOne(sequelize.models.sections, {
as: 'category',
foreignKey: 'category_id'
});
},
},
节-model.js
classMethods: {
associate() {
section.belongsTo(sequelize.models.categories, {
allowNull: true
});
},
},
services\index.js
...
app.set('models', sequelize.models);
...
Object.keys(sequelize.models).forEach(function(modelName) {
if ("associate" in sequelize.models[modelName]) {
sequelize.models[modelName].associate();
}
});
在这种情况下,我使用的是 Jetbrains webstorm。所以我做了一个 npm start,我的 table 现在有了正确的外键,所以那部分工作了。此外,数据的显示仍然是正确的。获取这些部分的查询仍在运行。如果我在 index.js 中有不同的编码,那么 npm start 不会失败,但部分查询失败。
接下来是:挂钩。这就是我现在感到困惑的地方。有些网站说它在 'find definition' 中(我们暂时这么称呼它,但这是在我的 vue 组件安装部分)。然后你就得到了解释,显示得很好,包含在那个部分,但它什么都不做,代码仍然运行,但是当我通过 postman 获取这些部分时,我没有看到类别信息。然后我会有例如
serviceSections.find({
include: [{
model: serviceCategories.Model
}],
query: {
$sort: {
section_description_en: 1
}
}
}).then(page => {
page.data.reverse();
this.listSections = page.data;
})
serviceCategories 定义为 "appFeathers.service('categories');"。如前所述,它什么都不做。所以回到我在这里得到的解释,它说'.. from a before hook ..'。我找到了 hooks\index.js 文件,用于类别和服务。但在这里我犯了错误。我首先在类别中进行了此调整,然后在第一个
部分进行了调整exports.before = {
...
find: [{
function (hook) {
if (hook.params.query.include) {
const AssociatedModel = hook.app.services.category.Model;
hook.params.sequelize = {
include: [{ model: AssociatedModel }]
};
}
return Promise.resolve(hook);
}
}],
...
};
这是代码 500 fn.bind 错误。
只想到 post 进度,这并不意味着我停止寻找最后一步(或缺失的一步)。
我忘了一件事。我检查它是否完成是在chrome中打开F12,进入VUE插件并展开'listSections'这是"serviceSections.find"的结果。我希望在其中看到类别列,但也许这是错误的期望。我也没有在我的调试器
的 select 中看到 'join'稍后编辑:
好吧,所以我搞砸了。最后我也遇到了这个poston how to retrieve data from many to many relationships。读到这里,我认为调整 "hooks\index.js" 的意图是正确的,但这意味着我的代码不是。所以尝试 post 和上面提供的提示的不同组合,我现在有了这个
节\hooks\index.js
...
exports.before = {
all: [],
find: [
getCategory()
],
get: [
getCategory()
],
create: [],
update: [],
patch: [],
remove: []
};
...
function getCategory() {
return function (hook) {
const category = hook.app.services.categories.Model;
hook.params.sequelize = {
include: [{ model: category }]
};
return Promise.resolve(hook);
};
}
这是在 postman 中使用 GET 工作的,因为将它放在 'before' 部分的 'get' 部分中,它在 VUE 中工作,因为我将函数放在'find' 'before'.
的一部分多重连接
好的,我需要多个加入 'sections'。我也有地位。这来自我的元数据 table。所以这意味着对元数据模型中的部分进行相同的关联。我是这样做的:
元数据-model.js
classMethods: {
associate() {
metadata.hasOne(sequelize.models.sections, {
as: 'satus',
foreignKey: 'status_id',
targetKey: 'status_id'
});
}, },
这里我开始总是放入foreignKey属性。 targetKey 是另一个表中列的名称。如果你需要改变它很方便。 'as' 属性是一个别名,我喜欢几乎总是使用它,至少在多次使用它时是这样。在部分模型中,我进行了更改。
节-model.js
classMethods: {
associate() {
section.belongsTo(sequelize.models.categories, {
allowNull: false,
as: 'category'
});
section.belongsTo(sequelize.models.metadata, {
allowNull: false,
as: 'status'
});
}, },
为了完成这个,我更改了 hooks 函数。我首先尝试了太函数,但那没有用,所以我将两者合并。
节\hooks\index.js
function getRelatedInfo() {
return function (hook) {
hook.params.sequelize = {
include: [
{
model: hook.app.services.categories.Model,
as: 'category'
},{
model: hook.app.services.metadata.Model,
as: 'status',
where: {
type: 'status'
}
}
]
};
return Promise.resolve(hook);
};
}
如您所见,我更改了函数名称。再次使用别名很重要,它在其他方面不起作用。在元数据部分,我放了一个过滤器。我不想在查找时加载整个 table。好的,我加入了一个 id,所以它确实是一对一的,但是如果由于某种原因元数据条目更改为不同的类型,我仍然在 'status' 上有 selection 并且可以错误行的警告。
最后一部分是您想要内部联接还是左外部联接。我通过钩子部分中的 'required: true' 属性触发了它。
VUE.js部分
最后一部分是将其推送到一个vue组件。我在安装部分这样做。我有这个代码。
const socket = io();
const appFeathers = feathers()
.configure(feathers.socketio(socket))
.configure(feathers.hooks());
const serviceSections = appFeathers.service('sections');
const serviceArticles = appFeathers.service('articles');
const serviceMetadata = appFeathers.service('metadata');
...
mounted() {
serviceArticles.find({
include: [{
model: serviceMetadata.Model,
as: 'country',
query: {
$select: [
'country_icon'
]
}
}],
query: {
$sort: {
article_description_en: 1
},
$select: [
'id',
['article_description_en', 'article_description'],
'article_length',
'article_ascend',
'article_code'
]
}
}).then(page => {
this.listTrails = page.data;
})
}
我在这里所做的是从数组中过滤不需要的列。我也重命名了一些。 '*_en' 是多语言的,所以我需要为它使用一个变量。再次重复包含以从连接中获取相关列。
从 Sequelize 的 v4 开始,classMethods
属性 已被删除 See this reference。这意味着@Edgar 的回答中的示例将不起作用。
associate
方法必须直接添加到模型中,而不是包裹在 classMethods
属性 中。来自 Sequelize 文档:
Previous:
const Model = sequelize.define('Model', { ... }, { classMethods: { associate: function (model) {...} }, instanceMethods: { someMethod: function () { ...} } });
New:
const Model = sequelize.define('Model', { ... }); // Class Method Model.associate = function (models) { ...associate the models }; // Instance Method Model.prototype.someMethod = function () {..}