implemented mongo indices

This commit is contained in:
Umed Khudoiberdiev 2017-02-23 12:20:11 +05:00
parent 0ae68e49aa
commit 665c9e0f55
9 changed files with 132 additions and 10 deletions

View File

@ -249,8 +249,12 @@ export class Connection {
if (dropBeforeSync)
await this.dropDatabase();
if (!(this.driver instanceof MongoDriver)) // todo: temporary
if (this.driver instanceof MongoDriver) { // todo: temporary
await this.driver.syncSchema(this.entityMetadatas);
} else {
await this.createSchemaBuilder().build();
}
}
/**

View File

@ -25,21 +25,21 @@ export function Index(fields: string[], options?: IndexOptions): Function;
/**
* Composite index must be set on entity classes and must specify entity's fields to be indexed.
*/
export function Index(fields: (object: any) => any[], options?: IndexOptions): Function;
export function Index(fields: (object?: any) => (any[]|{ [key: string]: number }), options?: IndexOptions): Function;
/**
* Composite index must be set on entity classes and must specify entity's fields to be indexed.
*/
export function Index(name: string, fields: (object: any) => any[], options?: IndexOptions): Function;
export function Index(name: string, fields: (object?: any) => (any[]|{ [key: string]: number }), options?: IndexOptions): Function;
/**
* Composite index must be set on entity classes and must specify entity's fields to be indexed.
*/
export function Index(nameOrFieldsOrOptions: string|string[]|((object: any) => any[])|IndexOptions,
maybeFieldsOrOptions?: ((object: any) => any[])|IndexOptions|string[],
maybeFieldsOrOptions?: ((object?: any) => (any[]|{ [key: string]: number }))|IndexOptions|string[],
maybeOptions?: IndexOptions): Function {
const name = typeof nameOrFieldsOrOptions === "string" ? nameOrFieldsOrOptions : undefined;
const fields = typeof nameOrFieldsOrOptions === "string" ? <((object: any) => any[])|string[]> maybeFieldsOrOptions : nameOrFieldsOrOptions as string[];
const fields = typeof nameOrFieldsOrOptions === "string" ? <((object?: any) => (any[]|{ [key: string]: number }))|string[]> maybeFieldsOrOptions : nameOrFieldsOrOptions as string[];
let options = (typeof nameOrFieldsOrOptions === "object" && !Array.isArray(nameOrFieldsOrOptions)) ? nameOrFieldsOrOptions as IndexOptions : maybeOptions;
if (!options)
options = (typeof maybeFieldsOrOptions === "object" && !Array.isArray(maybeFieldsOrOptions)) ? nameOrFieldsOrOptions as IndexOptions : maybeOptions;

View File

@ -1,5 +1,4 @@
import {ColumnOptions} from "../options/ColumnOptions";
import {ColumnTypeUndefinedError} from "../error/ColumnTypeUndefinedError";
import {GeneratedOnlyForPrimaryError} from "../error/GeneratedOnlyForPrimaryError";
import {getMetadataArgsStorage} from "../../index";
import {ColumnType, ColumnTypes} from "../../metadata/types/ColumnTypes";

View File

@ -8,4 +8,11 @@ export interface IndexOptions {
*/
readonly unique?: boolean;
/**
* If true, the index only references documents with the specified field.
* These indexes use less space but behave differently in some situations (particularly sorts).
* This option is only supported for mongodb database.
*/
readonly sparse?: boolean;
}

View File

@ -11,6 +11,7 @@ import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {PlatformTools} from "../../platform/PlatformTools";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {EntityMetadata} from "../../metadata/EntityMetadata";
/**
* Organizes communication with MongoDB.
@ -216,6 +217,20 @@ export class MongoDriver implements Driver {
return value;
}
// todo: make better abstraction
async syncSchema(entityMetadatas: EntityMetadata[]): Promise<void> {
const queryRunner = await this.createQueryRunner() as MongoQueryRunner;
const promises: Promise<any>[] = [];
await Promise.all(entityMetadatas.map(metadata => {
metadata.indices.forEach(index => {
const columns = index.buildColumnsAsMap(1);
const options = { name: index.name };
promises.push(queryRunner.createCollectionIndex(metadata.table.name, columns, options));
});
}));
await Promise.all(promises);
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------

View File

@ -16,7 +16,7 @@ export interface IndexMetadataArgs {
/**
* Columns combination to be used as index.
*/
readonly columns: ((object: any) => any[])|string[];
readonly columns: ((object?: any) => (any[]|{ [key: string]: number }))|string[];
/**
* Indicates if index must be unique or not.

View File

@ -41,7 +41,7 @@ export class IndexMetadata {
/**
* Columns combination to be used as index.
*/
private readonly _columns: ((object: any) => any[])|string[];
private readonly _columns: ((object?: any) => (any[]|{ [key: string]: number }))|string[];
// ---------------------------------------------------------------------
// Constructor
@ -84,12 +84,14 @@ export class IndexMetadata {
} else {
// if columns is a function that returns array of field names then execute it and get columns names from it
const propertiesMap = this.entityMetadata.createPropertiesMap();
columnPropertyNames = this._columns(propertiesMap).map((i: any) => String(i));
const columnsFnResult = this._columns(propertiesMap);
const columnsNamesFromFnResult = columnsFnResult instanceof Array ? columnsFnResult : Object.keys(columnsFnResult);
columnPropertyNames = columnsNamesFromFnResult.map((i: any) => String(i));
}
const columns = this.entityMetadata.columns.filter(column => columnPropertyNames.indexOf(column.propertyName) !== -1);
const missingColumnNames = columnPropertyNames.filter(columnPropertyName => !this.entityMetadata.columns.find(column => column.propertyName === columnPropertyName));
if (missingColumnNames.length > 0) {
if (missingColumnNames.length > 0) { // todo: better to extract all validation into single place is possible
// console.log(this.entityMetadata.columns);
throw new Error(`Index ${this._name ? "\"" + this._name + "\" " : ""}contains columns that are missing in the entity: ` + missingColumnNames.join(", "));
}
@ -97,4 +99,38 @@ export class IndexMetadata {
return columns.map(column => column.fullName);
}
/**
* Builds columns as a map of values where column name is key of object and value is a value provided by
* function or default value given to this function.
*/
buildColumnsAsMap(defaultValue = 0): { [key: string]: number } {
const map: { [key: string]: number } = {};
// if columns already an array of string then simply create a map from it
if (this._columns instanceof Array) {
this._columns.forEach(columnName => map[columnName] = defaultValue);
} else {
// if columns is a function that returns array of field names then execute it and get columns names from it
const propertiesMap = this.entityMetadata.createPropertiesMap();
const columnsFnResult = this._columns(propertiesMap);
if (columnsFnResult instanceof Array) {
columnsFnResult.forEach(columnName => map[columnName] = defaultValue);
} else {
Object.keys(columnsFnResult).forEach(columnName => map[columnName] = columnsFnResult[columnName]);
}
}
// replace each propertyNames with column names
return Object.keys(map).reduce((updatedMap, key) => {
const column = this.entityMetadata.columns.find(column => column.propertyName === key);
if (!column)
throw new Error(`Index ${this._name ? "\"" + this._name + "\" " : ""}contains columns that are missing in the entity: ${key}`);
updatedMap[column.name] = map[key];
return updatedMap;
}, {} as { [key: string]: number });
}
}

View File

@ -0,0 +1,29 @@
import {Entity} from "../../../../../../src/decorator/entity/Entity";
import {Column} from "../../../../../../src/decorator/columns/Column";
import {ObjectID} from "mongodb";
import {ObjectIdColumn} from "../../../../../../src/decorator/columns/ObjectIdColumn";
import {Index} from "../../../../../../src/decorator/Index";
@Entity()
@Index(["title", "name"])
@Index(() => ({ title: -1, name: -1, count: 1 }))
@Index("title_name_count", () => ({ title: 1, name: 1, count: 1 }))
@Index("title_name_count_reversed", () => ({ title: -1, name: -1, count: -1 }))
export class Post {
@ObjectIdColumn()
id: ObjectID;
@Column()
@Index()
title: string;
@Column()
@Index()
name: string;
@Column()
@Index()
count: number;
}

View File

@ -0,0 +1,32 @@
import "reflect-metadata";
import {Connection} from "../../../../../src/connection/Connection";
import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../../../utils/test-utils";
import {Post} from "./entity/Post";
import {expect} from "chai";
describe("mongodb > indices", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Post],
enabledDrivers: ["mongodb"]
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should insert entity with indices correctly", () => Promise.all(connections.map(async connection => {
const postRepository = connection.getRepository(Post);
// save a post
const post = new Post();
post.title = "Post";
post.name = "About Post";
await postRepository.persist(post);
// check saved post
const loadedPost = await postRepository.findOne({ title: "Post" });
expect(loadedPost).to.be.not.empty;
})));
});