first step: introduce composite primary keys support

This commit is contained in:
Umed Khudoiberdiev 2016-09-06 16:10:54 +05:00
parent 07f1b4b9b1
commit e64dd9576d
13 changed files with 103 additions and 38 deletions

View File

@ -0,0 +1,39 @@
import "reflect-metadata";
import {createConnection, ConnectionOptions} from "../../src/index";
import {Post} from "./entity/Post";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "test"
},
logging: {
logOnlyFailedQueries: true,
logFailedQueryError: true
},
autoSchemaCreate: true,
entities: [Post]
};
createConnection(options).then(connection => {
let postRepository = connection.getRepository(Post);
const post = new Post();
post.id = 1;
post.type = "person";
post.text = "this is test post";
postRepository.persist(post)
.then(savedPost => {
console.log("Post has been saved: ", savedPost);
})
.catch(error => {
console.log("error: ", error);
});
}, error => console.log("Cannot connect: ", error));

View File

@ -0,0 +1,15 @@
import {PrimaryColumn, Column, Table} from "../../../src/index";
@Table("sample27_composite_primary_keys")
export class Post {
@PrimaryColumn("int")
id: number;
@PrimaryColumn()
type: string;
@Column()
text: string;
}

View File

@ -56,7 +56,7 @@ export interface QueryRunner {
/**
* Inserts a new row into given table.
*/
insert(tableName: string, valuesMap: Object, idColumn?: ColumnMetadata): Promise<any>;
insert(tableName: string, valuesMap: Object, generatedColumn?: ColumnMetadata): Promise<any>;
/**
* Performs a simple DELETE query by a given conditions in a given table.

View File

@ -163,7 +163,7 @@ export class MariaDbQueryRunner implements QueryRunner {
/**
* Insert a new row with given values into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise<any> {
async insert(tableName: string, keyValues: ObjectLiteral, generatedColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
@ -173,9 +173,9 @@ export class MariaDbQueryRunner implements QueryRunner {
const parameters = keys.map(key => keyValues[key]);
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`;
const result = await this.query(sql, parameters);
if (result.info && idColumn) {
if (result.info && generatedColumn) {
const id = result.info.insertId;
if (ColumnTypes.isNumeric(idColumn.type)) {
if (ColumnTypes.isNumeric(generatedColumn.type)) {
return +id;
}
return id;

View File

@ -148,7 +148,7 @@ export class MysqlQueryRunner implements QueryRunner {
/**
* Insert a new row with given values into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise<any> {
async insert(tableName: string, keyValues: ObjectLiteral, generatedColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
@ -158,7 +158,7 @@ export class MysqlQueryRunner implements QueryRunner {
const parameters = keys.map(key => keyValues[key]);
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`;
const result = await this.query(sql, parameters);
return idColumn ? result.insertId : undefined;
return generatedColumn ? result.insertId : undefined;
}
/**
@ -318,7 +318,11 @@ export class MysqlQueryRunner implements QueryRunner {
throw new QueryRunnerAlreadyReleasedError();
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, false)).join(", ");
const sql = `CREATE TABLE \`${table.name}\` (${columnDefinitions}) ENGINE=InnoDB;`;
let sql = `CREATE TABLE \`${table.name}\` (${columnDefinitions}`;
const primaryKeyColumns = columns.filter(column => column.isPrimary);
if (primaryKeyColumns.length > 0)
sql += `, PRIMARY KEY(${primaryKeyColumns.map(column => `\`${column.name}\``).join(", ")})`;
sql += `) ENGINE=InnoDB;`;
await this.query(sql);
return columns;
}
@ -507,8 +511,8 @@ export class MysqlQueryRunner implements QueryRunner {
c += " NOT NULL";
if (column.isUnique === true)
c += " UNIQUE";
if (column.isPrimary === true && !skipPrimary)
c += " PRIMARY KEY";
// if (column.isPrimary === true && !skipPrimary)
// c += " PRIMARY KEY";
if (column.isGenerated === true) // don't use skipPrimary here since updates can update already exist primary without auto inc.
c += " AUTO_INCREMENT";
if (column.comment)

View File

@ -156,7 +156,7 @@ export class OracleQueryRunner implements QueryRunner {
/**
* Insert a new row with given values into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise<any> {
async insert(tableName: string, keyValues: ObjectLiteral, generatedColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
@ -164,10 +164,10 @@ export class OracleQueryRunner implements QueryRunner {
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map(key => ":" + key).join(",");
const parameters = keys.map(key => keyValues[key]);
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ idColumn ? " RETURNING " + this.driver.escapeColumnName(idColumn.name) : "" }`;
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ generatedColumn ? " RETURNING " + this.driver.escapeColumnName(generatedColumn.name) : "" }`;
const result = await this.query(sql, parameters);
if (idColumn)
return result[0][idColumn.name];
if (generatedColumn)
return result[0][generatedColumn.name];
return result;
}

View File

@ -141,18 +141,18 @@ export class PostgresQueryRunner implements QueryRunner {
/**
* Insert a new row into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise<any> {
async insert(tableName: string, keyValues: ObjectLiteral, generatedColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const keys = Object.keys(keyValues);
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map((key, index) => "$" + (index + 1)).join(",");
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ idColumn ? " RETURNING " + this.driver.escapeColumnName(idColumn.name) : "" }`;
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ generatedColumn ? " RETURNING " + this.driver.escapeColumnName(generatedColumn.name) : "" }`;
const parameters = keys.map(key => keyValues[key]);
const result: ObjectLiteral[] = await this.query(sql, parameters);
if (idColumn)
return result[0][idColumn.name];
if (generatedColumn)
return result[0][generatedColumn.name];
return result;
}

View File

@ -141,7 +141,7 @@ export class SqliteQueryRunner implements QueryRunner {
/**
* Insert a new row into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise<any> {
async insert(tableName: string, keyValues: ObjectLiteral, generatedColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
@ -162,7 +162,7 @@ export class SqliteQueryRunner implements QueryRunner {
_this.logger.logQueryError(err);
fail(err);
} else {
if (idColumn)
if (generatedColumn)
return ok(this["lastID"]);
ok();

View File

@ -148,7 +148,7 @@ export class SqlServerQueryRunner implements QueryRunner {
/**
* Insert a new row with given values into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise<any> {
async insert(tableName: string, keyValues: ObjectLiteral, generatedColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
@ -158,7 +158,7 @@ export class SqlServerQueryRunner implements QueryRunner {
const parameters = keys.map(key => keyValues[key]);
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`;
const result = await this.query(sql, parameters);
return idColumn ? result.insertId : undefined;
return generatedColumn ? result.insertId : undefined;
}
/**

View File

@ -134,16 +134,10 @@ export class EntityMetadata {
return "";
}
/**
* Checks if entity has a primary column. All user's entities must have a primary column.
* Special entity metadatas like for junction tables and closure junction tables don't have a primary column.
*/
get hasPrimaryColumn(): boolean {
return !!this._columns.find(column => column.isPrimary);
}
/**
* Gets the primary column.
*
* @deprecated
*/
get primaryColumn(): ColumnMetadata {
const primaryKey = this._columns.find(column => column.isPrimary);
@ -153,6 +147,13 @@ export class EntityMetadata {
return primaryKey;
}
/**
* Gets the primary columns.
*/
get primaryColumns(): ColumnMetadata[] {
return this._columns.filter(column => column.isPrimary);
}
/**
* Checks if entity has a create date column.
*/
@ -457,5 +458,11 @@ export class EntityMetadata {
&& object.hasOwnProperty(relation.propertyName);
});
}
checkIfObjectContainsAllPrimaryKeys(object: ObjectLiteral) {
return this.primaryColumns.every(primaryColumn => {
return object.hasOwnProperty(primaryColumn.propertyName);
});
}
}

View File

@ -9,10 +9,8 @@ import {Broadcaster} from "../subscriber/Broadcaster";
import {EntityMetadataCollection} from "../metadata-args/collection/EntityMetadataCollection";
import {Driver} from "../driver/Driver";
import {UpdateByInverseSideOperation} from "./operation/UpdateByInverseSideOperation";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {RelationMetadata} from "../metadata/RelationMetadata";
import {ObjectLiteral} from "../common/ObjectLiteral";
import {DatabaseConnection} from "../driver/DatabaseConnection";
import {QueryRunner} from "../driver/QueryRunner";
/**
@ -479,8 +477,8 @@ export class PersistOperationExecutor {
}*/
// console.log("inserting: ", this.zipObject(allColumns, allValues));
let idColumn = metadata.hasPrimaryColumn && metadata.primaryColumn.isGenerated ? metadata.primaryColumn : undefined;
return this.queryRunner.insert(metadata.table.name, this.zipObject(allColumns, allValues), idColumn);
let generatedColumn = metadata.columns.find(column => column.isGenerated);
return this.queryRunner.insert(metadata.table.name, this.zipObject(allColumns, allValues), generatedColumn);
}
private insertIntoClosureTable(operation: InsertOperation, updateMap: ObjectLiteral) {

View File

@ -23,7 +23,7 @@ export class PlainObjectToDatabaseEntityTransformer<Entity extends ObjectLiteral
async transform(plainObject: ObjectLiteral, metadata: EntityMetadata, queryBuilder: QueryBuilder<Entity>): Promise<Entity> {
// if plain object does not have id then nothing to load really
if (!metadata.hasPrimaryColumn || !plainObject.hasOwnProperty(metadata.primaryColumn.name))
if (!metadata.checkIfObjectContainsAllPrimaryKeys(plainObject))
return Promise.reject<Entity>("Given object does not have a primary column, cannot transform it to database entity.");
const alias = queryBuilder.alias;
@ -31,9 +31,11 @@ export class PlainObjectToDatabaseEntityTransformer<Entity extends ObjectLiteral
this.join(queryBuilder, needToLoad, alias);
queryBuilder
.where(alias + "." + metadata.primaryColumn.name + "=:id")
.setParameter("id", plainObject[metadata.primaryColumn.name]);
metadata.primaryColumns.forEach(primaryColumn => {
queryBuilder
.where(alias + "." + primaryColumn.name + "=:" + primaryColumn.name)
.setParameter(primaryColumn.name, plainObject[primaryColumn.name]);
});
return await queryBuilder.getSingleResult();
}

View File

@ -300,7 +300,7 @@ export class SchemaBuilder {
*/
protected removePrimaryKeys() {
const queries = this.entityMetadatas
.filter(metadata => !metadata.hasPrimaryColumn)
.filter(metadata => !metadata.primaryColumns.length)
.map(async metadata => {
const dbTable = this.tableSchemas.find(table => table.name === metadata.table.name);
if (dbTable && dbTable.primaryKey) {