不要怂,就是干,撸起袖子干!

Commit a6a8146c by Jan Aagaard Meier

feat(querying) Add options.include and .exclude. Closes #4074

1 parent 5e5f00f6
......@@ -9,6 +9,7 @@
- [FIXED] Add limit to `findOne` when using queries like `{ id: { $gt ...` [#4416](https://github.com/sequelize/sequelize/issues/4416)
- [FIXED] Include all with scopes [#4584](https://github.com/sequelize/sequelize/issues/4584)
- [INTERNALS] Corrected spelling seperate -> separate
- [ADDED] Added `include` and `exclude` to `options.attributes`. [#4074](https://github.com/sequelize/sequelize/issues/4074)
# 3.10.0
- [ADDED] support `search_path` for postgres with lots of schemas [#4534](https://github.com/sequelize/sequelize/pull/4534)
......
## Attributes
To select only some attributes, you can use the `attributes` option. Most often, you pass an array:
```js
Model.findAll({
attributes: ['foo', 'bar']
});
```
```sql
SELECT foo, bar ...
```
Attributes can be renamed using a nested array:
```js
Model.findAll({
attributes: ['foo', ['bar', 'baz']]
});
```
```sql
SELECT foo, bar AS baz ...
```
You can use `sequelize.fn` to do aggregations:
```js
Model.findAll({
attributes: [sequelize.fn('COUNT', sequelize.col('hats')), 'no_hats']
});
```
```sql
SELECT COUNT(hats) AS no_hats ...
```
When using aggregation function, you must give it an alias to be able to access it from the model. In the example above you can get the number of hats with `instance.get('no_hats')`.
Sometimes it may be tiresome to list all the attributes of the model if you only want to add an aggregation:
```js
// This is a tiresome way of getting the number of hats...
Model.findAll({
attributes: ['id', 'foo', 'bar', 'baz', 'quz', sequelize.fn('COUNT', sequelize.col('hats')), 'no_hats']
});
// This is shorter, and less error prone because it still works if you add / remove attributes
Model.findAll({
attributes: { include: [sequelize.fn('COUNT', sequelize.col('hats')), 'no_hats']] }
});
```
```sql
SELECT id, foo, bar, baz, quz, COUNT(hats) AS no_hats ...
```
Similarly, its also possible to remove a selected few attributes:
```js
Model.findAll({
attributes: { exclude: ['baz'] }
});
```
```sql
SELECT id, foo, bar, quz ...
```
## Where
Whether you are querying with findAll/find or doing bulk updates/destroys you can pass a `where` object to filter the query.
......
......@@ -518,27 +518,33 @@ Model.$validateIncludedElements = validateIncludedElements;
validateIncludedElement = function(include, tableNames, options) {
tableNames[include.model.getTableName()] = true;
// Need to make sure virtuals are mapped before setting originalAttributes
include = Utils.mapFinderOptions(include, include.model);
if (include.attributes && !options.raw) {
include.model.$handleAttributes(include);
// Need to make sure virtuals are mapped before setting originalAttributes
include = Utils.mapFinderOptions(include, include.model);
include.originalAttributes = include.attributes.slice(0);
if (include.attributes.length) {
_.each(include.model.primaryKeys, function (attr, key) {
// Include the primary key if its not already take - take into account that the pk might be aliassed (due to a .field prop)
if (!_.any(include.attributes, function (includeAttr) {
if (attr.field !== key) {
return Array.isArray(includeAttr) && includeAttr[0] === attr.field && includeAttr[1] === key;
}
return includeAttr === key;
})) {
if (attr.field !== key) {
return Array.isArray(includeAttr) && includeAttr[0] === attr.field && includeAttr[1] === key;
}
return includeAttr === key;
})) {
include.attributes.unshift(key);
}
});
}
} else if (!include.attributes) {
include.attributes = Object.keys(include.model.tableAttributes);
} else {
include = Utils.mapFinderOptions(include, include.model);
if (!include.attributes) {
include.attributes = Object.keys(include.model.tableAttributes);
}
}
// pseudo include just needed the attribute logic, return
......@@ -1269,7 +1275,9 @@ Model.prototype.all = function(options) {
*
* @param {Object} [options] A hash of options to describe the scope of the search
* @param {Object} [options.where] A hash of attributes to describe your search. See above for examples.
* @param {Array<String>} [options.attributes] A list of the attributes that you want to select. To rename an attribute, you can pass an array, with two elements - the first is the name of the attribute in the DB (or some kind of expression such as `Sequelize.literal`, `Sequelize.fn` and so on), and the second is the name you want the attribute to have in the returned instance
* @param {Array<String>|Object} [options.attributes] A list of the attributes that you want to select, or an object with `include` and `exclude` keys. To rename an attribute, you can pass an array, with two elements - the first is the name of the attribute in the DB (or some kind of expression such as `Sequelize.literal`, `Sequelize.fn` and so on), and the second is the name you want the attribute to have in the returned instance
* @param {Array<String>} [options.attributes.include] Select all the attributes of the model, plus some additional ones. Useful for aggregations, e.g. `{ attributes: { include: [[sequelize.fn('COUNT', sequelize.col('id')), 'total)]] }`
* @param {Array<String>} [options.attributes.exclude] Select all the attributes of the model, except some few. Useful for security purposes e.g. `{ attributes: { exclude: ['password'] } }`
* @param {Boolean} [options.paranoid=true] If true, only non-deleted records will be returned. If false, both deleted and non-deleted records will be returned. Only applies if `options.paranoid` is true for the model.
* @param {Array<Object|Model>} [options.include] A list of associations to eagerly load using a left join. Supported is either `{ include: [ Model1, Model2, ...]}` or `{ include: [{ model: Model1, as: 'Alias' }]}`. If your association are set up with an `as` (eg. `X.hasMany(Y, { as: 'Z }`, you need to specify Z in the as attribute when eager loading Y).
* @param {Model} [options.include[].model] The model you want to eagerly load
......@@ -1328,28 +1336,22 @@ Model.prototype.findAll = function(options) {
return this.runHooks('beforeFindAfterExpandIncludeAll', options);
}
}).then(function() {
if (typeof options === 'object') {
if (options.include) {
options.hasJoin = true;
this.$handleAttributes(options);
validateIncludedElements.call(this, options, tableNames);
if (options.include) {
options.hasJoin = true;
// If we're not raw, we have to make sure we include the primary key for deduplication
if (options.attributes && !options.raw) {
if (options.attributes.indexOf(this.primaryKeyAttribute) === -1) {
options.originalAttributes = options.attributes;
options.attributes = [this.primaryKeyAttribute].concat(options.attributes);
}
}
}
validateIncludedElements.call(this, options, tableNames);
// whereCollection is used for non-primary key updates
this.options.whereCollection = options.where || null;
// If we're not raw, we have to make sure we include the primary key for deduplication
if (!options.raw && options.attributes.indexOf(this.primaryKeyAttribute) === -1) {
options.originalAttributes = options.attributes;
options.attributes = [this.primaryKeyAttribute].concat(options.attributes);
}
}
if (options.attributes === undefined) {
options.attributes = Object.keys(this.tableAttributes);
}
// whereCollection is used for non-primary key updates
this.options.whereCollection = options.where || null;
Utils.mapFinderOptions(options, this);
......@@ -2561,6 +2563,23 @@ Model.prototype.$getDefaultTimestamp = function(attr) {
return undefined;
};
Model.prototype.$handleAttributes = function (options) {
var attributes = Array.isArray(options.attributes) ? options.attributes : Object.keys(this.tableAttributes);
if (_.isPlainObject(options.attributes)) {
if (options.attributes.exclude) {
attributes = attributes.filter(function (elem) {
return options.attributes.exclude.indexOf(elem) === -1;
});
}
if (options.attributes.include) {
attributes = attributes.concat(options.attributes.include);
}
}
options.attributes = attributes;
};
// Inject current scope into options. Includes should have been conformed (conformOptions) before calling this
Model.$injectScope = function (scope, options) {
scope = optClone(scope);
......
......@@ -82,7 +82,7 @@ var Utils = module.exports = {
/* Expand and normalize finder options */
mapFinderOptions: function(options, Model) {
if (Model._hasVirtualAttributes && options.attributes) {
if (Model._hasVirtualAttributes && Array.isArray(options.attributes)) {
options.attributes.forEach(function (attribute) {
if (Model._isVirtualAttribute(attribute) && Model.rawAttributes[attribute].type.fields) {
options.attributes = options.attributes.concat(Model.rawAttributes[attribute].type.fields);
......@@ -99,7 +99,7 @@ var Utils = module.exports = {
/* Used to map field names in attributes and where conditions */
mapOptionFieldNames: function(options, Model) {
if (options.attributes) {
if (Array.isArray(options.attributes)) {
options.attributes = options.attributes.map(function(attr) {
// Object lookups will force any variable to strings, we don't want that for special objects etc
if (typeof attr !== 'string') return attr;
......
......@@ -3,7 +3,6 @@ site_description: Sequelize is an ORM for Node.js and io.js. It supports the dia
repo_url: https://github.com/sequelize/sequelize
site_favicon: favicon.ico
site_url: http://docs.sequelizejs.com
markdown_extensions: [gfm]
theme: readthedocs
extra_css:
- css/custom.css
......
'use strict';
/* jshint -W030 */
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, current = Support.sequelize
, sinon = require('sinon')
, DataTypes = require(__dirname + '/../../../lib/data-types');
describe(Support.getTestDialectTeaser('Model'), function() {
describe('method findAll', function () {
var Model = current.define('model', {
name: DataTypes.STRING
}, { timestamps: false });
before(function () {
this.stub = sinon.stub(current.getQueryInterface(), 'select', function () {
return Model.build({});
});
});
beforeEach(function () {
this.stub.reset();
});
after(function () {
this.stub.restore();
});
describe('attributes include / exclude', function () {
it('allows me to include additional attributes', function () {
return Model.findAll({
attributes: {
include: ['foobar']
}
}).bind(this).then(function () {
expect(this.stub.getCall(0).args[2].attributes).to.deep.equal([
'id',
'name',
'foobar'
]);
});
});
it('allows me to exclude attributes', function () {
return Model.findAll({
attributes: {
exclude: ['name']
}
}).bind(this).then(function () {
expect(this.stub.getCall(0).args[2].attributes).to.deep.equal([
'id'
]);
});
});
it('include takes precendence over exclude', function () {
return Model.findAll({
attributes: {
exclude: ['name'],
include: ['name']
}
}).bind(this).then(function () {
expect(this.stub.getCall(0).args[2].attributes).to.deep.equal([
'id',
'name'
]);
});
});
});
});
});
......@@ -66,6 +66,75 @@ describe(Support.getTestDialectTeaser('Model'), function() {
// Calling validate again shouldn't add the pk again
expect(options.include[0].attributes).to.deep.equal([['field_id', 'id'], 'name']);
});
describe('include / exclude', function () {
it('allows me to include additional attributes', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{
model: this.Company,
attributes: {
include: ['foobar']
}
}
]
});
expect(options.include[0].attributes).to.deep.equal([
['field_id', 'id'],
'name',
'createdAt',
'updatedAt',
'ownerId',
'foobar'
]);
});
it('allows me to exclude attributes', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{
model: this.Company,
attributes: {
exclude: ['name']
}
}
]
});
expect(options.include[0].attributes).to.deep.equal([
['field_id', 'id'],
'createdAt',
'updatedAt',
'ownerId'
]);
});
it('include takes precendence over exclude', function () {
var options = Sequelize.Model.$validateIncludedElements({
model: this.User,
include: [
{
model: this.Company,
attributes: {
exclude: ['name'],
include: ['name']
}
}
]
});
expect(options.include[0].attributes).to.deep.equal([
['field_id', 'id'],
'createdAt',
'updatedAt',
'ownerId',
'name'
]);
});
});
});
describe('scope', function () {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!