Skip to content

Commit

Permalink
feat: option to disable foreign keys creation (#7277)
Browse files Browse the repository at this point in the history
* Add relation option "createForeignKeyConstraints"

* feat: add createForeignKeyConstraints relation option

This new option allows to create relation without using foreign keys

Closes: #3120

* test: add cases for createForeignKeyConstraints relation option

* docs: createForeignKeyConstraints relation option

* test: remove .only

* chore: remove unused test files

* test: add case checking if relation without foreign key works as expected

* fix: register join columns for relation without foreign key

Closes: #3120

Co-authored-by: Alexander Bolshakov <alexanderb@cubedmobile.com>
  • Loading branch information
Saniol and alpharder committed Feb 8, 2021
1 parent b466edd commit cb17b95
Show file tree
Hide file tree
Showing 15 changed files with 339 additions and 15 deletions.
30 changes: 30 additions & 0 deletions docs/relations-faq.md
Expand Up @@ -4,6 +4,7 @@
* [How to use relation id without joining relation](#how-to-use-relation-id-without-joining-relation)
* [How to load relations in entities](#how-to-load-relations-in-entities)
* [Avoid relation property initializers](#avoid-relation-property-initializers)
* [Avoid foreign key constraint creation](#avoid-foreign-key-constraint-creation)

## How to create self referencing relation

Expand Down Expand Up @@ -214,3 +215,32 @@ Therefore, saving an object like this will bring you problems - it will remove a

How to avoid this behaviour? Simply don't initialize arrays in your entities.
Same rule applies to a constructor - don't initialize it in a constructor as well.

## Avoid foreign key constraint creation

Sometimes for performance reasons you might want to have a relation between entities, but without foreign key constraint.
You can define if foreign key constraint should be created with `createForeignKeyConstraints` option (default: true).

```typescript
import {Entity, PrimaryColumn, Column, ManyToOne} from "typeorm";
import {Person} from "./Person";

@Entity()
export class ActionLog {

@PrimaryColumn()
id: number;

@Column()
date: Date;

@Column()
action: string;

@ManyToOne(type => Person, {
createForeignKeyConstraints: false
})
person: Person;

}
```
7 changes: 7 additions & 0 deletions src/decorator/options/RelationOptions.ts
Expand Up @@ -42,6 +42,13 @@ export interface RelationOptions {
*/
primary?: boolean;

/**
* Indicates whether foreign key constraints will be created for join columns.
* Can be used only for many-to-one and owner one-to-one relations.
* Defaults to true.
*/
createForeignKeyConstraints?: boolean;

/**
* Set this relation to be lazy. Note: lazy relations are promises. When you call them they return promise
* which resolve relation result then. If your property's type is Promise then this relation is set to lazy automatically.
Expand Down
9 changes: 8 additions & 1 deletion src/metadata-builder/EntityMetadataBuilder.ts
Expand Up @@ -125,11 +125,14 @@ export class EntityMetadataBuilder {
// create entity's relations join columns (for many-to-one and one-to-one owner)
entityMetadata.relations.filter(relation => relation.isOneToOne || relation.isManyToOne).forEach(relation => {
const joinColumns = this.metadataArgsStorage.filterJoinColumns(relation.target, relation.propertyName);
const { foreignKey, uniqueConstraint } = this.relationJoinColumnBuilder.build(joinColumns, relation); // create a foreign key based on its metadata args
const { foreignKey, columns, uniqueConstraint } = this.relationJoinColumnBuilder.build(joinColumns, relation); // create a foreign key based on its metadata args
if (foreignKey) {
relation.registerForeignKeys(foreignKey); // push it to the relation and thus register there a join column
entityMetadata.foreignKeys.push(foreignKey);
}
if (columns) {
relation.registerJoinColumns(columns);
}
if (uniqueConstraint) {
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof AuroraDataApiDriver
|| this.connection.driver instanceof SqlServerDriver || this.connection.driver instanceof SapDriver) {
Expand Down Expand Up @@ -193,6 +196,10 @@ export class EntityMetadataBuilder {
// here we create a junction entity metadata for a new junction table of many-to-many relation
const junctionEntityMetadata = this.junctionEntityMetadataBuilder.build(relation, joinTable);
relation.registerForeignKeys(...junctionEntityMetadata.foreignKeys);
relation.registerJoinColumns(
junctionEntityMetadata.ownIndices[0].columns,
junctionEntityMetadata.ownIndices[1].columns
);
relation.registerJunctionEntityMetadata(junctionEntityMetadata);

// compute new entity metadata properties and push it to entity metadatas pool
Expand Down
4 changes: 2 additions & 2 deletions src/metadata-builder/JunctionEntityMetadataBuilder.ts
Expand Up @@ -138,7 +138,7 @@ export class JunctionEntityMetadataBuilder {
entityMetadata.ownColumns.forEach(column => column.relationMetadata = relation);

// create junction table foreign keys
entityMetadata.foreignKeys = [
entityMetadata.foreignKeys = relation.createForeignKeyConstraints ? [
new ForeignKeyMetadata({
entityMetadata: entityMetadata,
referencedEntityMetadata: relation.entityMetadata,
Expand All @@ -153,7 +153,7 @@ export class JunctionEntityMetadataBuilder {
referencedColumns: inverseReferencedColumns,
onDelete: relation.onDelete || "CASCADE"
}),
];
] : [];

// create junction table indices
entityMetadata.ownIndices = [
Expand Down
13 changes: 7 additions & 6 deletions src/metadata-builder/RelationJoinColumnBuilder.ts
Expand Up @@ -56,13 +56,14 @@ export class RelationJoinColumnBuilder {
*/
build(joinColumns: JoinColumnMetadataArgs[], relation: RelationMetadata): {
foreignKey: ForeignKeyMetadata|undefined,
columns: ColumnMetadata[],
uniqueConstraint: UniqueMetadata|undefined,
} {
const referencedColumns = this.collectReferencedColumns(joinColumns, relation);
if (!referencedColumns.length)
return { foreignKey: undefined, uniqueConstraint: undefined }; // this case is possible only for one-to-one non owning side

const columns = this.collectColumns(joinColumns, relation, referencedColumns);
if (!referencedColumns.length || !relation.createForeignKeyConstraints)
return { foreignKey: undefined, columns, uniqueConstraint: undefined }; // this case is possible for one-to-one non owning side and relations with createForeignKeyConstraints = false

const foreignKey = new ForeignKeyMetadata({
entityMetadata: relation.entityMetadata,
referencedEntityMetadata: relation.inverseEntityMetadata,
Expand All @@ -76,7 +77,7 @@ export class RelationJoinColumnBuilder {

// Oracle does not allow both primary and unique constraints on the same column
if (this.connection.driver instanceof OracleDriver && columns.every(column => column.isPrimary))
return { foreignKey, uniqueConstraint: undefined };
return { foreignKey, columns, uniqueConstraint: undefined };

// CockroachDB requires UNIQUE constraints on referenced columns
if (referencedColumns.length > 0 && relation.isOneToOne) {
Expand All @@ -89,10 +90,10 @@ export class RelationJoinColumnBuilder {
}
});
uniqueConstraint.build(this.connection.namingStrategy);
return {foreignKey, uniqueConstraint};
return {foreignKey, columns, uniqueConstraint};
}

return { foreignKey, uniqueConstraint: undefined };
return { foreignKey, columns, uniqueConstraint: undefined };
}
// -------------------------------------------------------------------------
// Protected Methods
Expand Down
19 changes: 17 additions & 2 deletions src/metadata/RelationMetadata.ts
Expand Up @@ -159,6 +159,13 @@ export class RelationMetadata {
*/
deferrable?: DeferrableType;

/**
* Indicates whether foreign key constraints will be created for join columns.
* Can be used only for many-to-one and owner one-to-one relations.
* Defaults to true.
*/
createForeignKeyConstraints: boolean = true;

/**
* Gets the property's type to which this relation is applied.
*
Expand Down Expand Up @@ -301,6 +308,7 @@ export class RelationMetadata {
this.onDelete = args.options.onDelete;
this.onUpdate = args.options.onUpdate;
this.deferrable = args.options.deferrable;
this.createForeignKeyConstraints = args.options.createForeignKeyConstraints === false ? false : true;
this.isEager = args.options.eager || false;
this.persistenceEnabled = args.options.persistence === false ? false : true;
this.orphanedRowAction = args.options.orphanedRowAction || "nullify";
Expand Down Expand Up @@ -495,8 +503,15 @@ export class RelationMetadata {
*/
registerForeignKeys(...foreignKeys: ForeignKeyMetadata[]) {
this.foreignKeys.push(...foreignKeys);
this.joinColumns = this.foreignKeys[0] ? this.foreignKeys[0].columns : [];
this.inverseJoinColumns = this.foreignKeys[1] ? this.foreignKeys[1].columns : [];
}

/**
* Registers given join columns in the relation.
* This builder method should be used to register join column in the relation.
*/
registerJoinColumns(joinColumns: ColumnMetadata[] = [], inverseJoinColumns: ColumnMetadata[] = []) {
this.joinColumns = joinColumns;
this.inverseJoinColumns = inverseJoinColumns;
this.isOwning = this.isManyToOne || ((this.isManyToMany || this.isOneToOne) && this.joinColumns.length > 0);
this.isOneToOneOwner = this.isOneToOne && this.isOwning;
this.isOneToOneNotOwner = this.isOneToOne && !this.isOwning;
Expand Down
3 changes: 2 additions & 1 deletion src/schema-builder/RdbmsSchemaBuilder.ts
Expand Up @@ -651,7 +651,8 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
if (!table)
continue;

const newKeys = metadata.foreignKeys.filter(foreignKey => {
const newKeys = metadata.foreignKeys
.filter(foreignKey => {
return !table.foreignKeys.find(dbForeignKey => foreignKeysMatch(dbForeignKey, foreignKey));
});
if (newKeys.length === 0)
Expand Down
6 changes: 3 additions & 3 deletions src/schema-builder/options/TableForeignKeyOptions.ts
Expand Up @@ -38,10 +38,10 @@ export interface TableForeignKeyOptions {
* referenced stuff is being updated.
*/
onUpdate?: string;

/**
* Set this foreign key constraint as "DEFERRABLE" e.g. check constraints at start
* Set this foreign key constraint as "DEFERRABLE" e.g. check constraints at start
* or at the end of a transaction
*/
deferrable?: string;
}
}
14 changes: 14 additions & 0 deletions test/github-issues/3120/entity/ActionDetails.ts
@@ -0,0 +1,14 @@
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";

@Entity()
export class ActionDetails {

@PrimaryGeneratedColumn()
id: number;

@Column()
description: string;

}
42 changes: 42 additions & 0 deletions test/github-issues/3120/entity/ActionLog.ts
@@ -0,0 +1,42 @@
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";
import {JoinColumn} from "../../../../src/decorator/relations/JoinColumn";
import {JoinTable} from "../../../../src/decorator/relations/JoinTable";
import {ManyToOne} from "../../../../src/decorator/relations/ManyToOne";
import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany";
import {OneToOne} from "../../../../src/decorator/relations/OneToOne";
import {ActionDetails} from "./ActionDetails";
import {Address} from "./Address";
import {Person} from "./Person";

@Entity()
export class ActionLog {

@PrimaryGeneratedColumn()
id: number;

@Column()
date: Date;

@Column()
action: string;

@ManyToOne(type => Person, {
createForeignKeyConstraints: false
})
person: Person;

@ManyToMany(type => Address, {
createForeignKeyConstraints: false
})
@JoinTable()
addresses: Address[];

@OneToOne(type => ActionDetails, {
createForeignKeyConstraints: false
})
@JoinColumn()
actionDetails: ActionDetails;

}
24 changes: 24 additions & 0 deletions test/github-issues/3120/entity/Address.ts
@@ -0,0 +1,24 @@
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";
import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany";
import {Person} from "./Person";

@Entity()
export class Address {

@PrimaryGeneratedColumn()
id: number;

@Column()
country: string;

@Column()
city: string;

@Column()
street: string;

@ManyToMany(type => Person, person => person.addresses)
people: Person[];
}
14 changes: 14 additions & 0 deletions test/github-issues/3120/entity/Company.ts
@@ -0,0 +1,14 @@
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";

@Entity()
export class Company {

@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

}
18 changes: 18 additions & 0 deletions test/github-issues/3120/entity/Passport.ts
@@ -0,0 +1,18 @@
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";
import {OneToOne} from "../../../../src/decorator/relations/OneToOne";
import {Person} from "./Person";

@Entity()
export class Passport {

@PrimaryGeneratedColumn()
id: number;

@Column()
passportNumber: string;

@OneToOne(type => Person, person => person.passport)
owner: Person;
}
33 changes: 33 additions & 0 deletions test/github-issues/3120/entity/Person.ts
@@ -0,0 +1,33 @@
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";
import {JoinColumn} from "../../../../src/decorator/relations/JoinColumn";
import {JoinTable} from "../../../../src/decorator/relations/JoinTable";
import {ManyToOne} from "../../../../src/decorator/relations/ManyToOne";
import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany";
import {OneToOne} from "../../../../src/decorator/relations/OneToOne";
import {Address} from "./Address";
import {Company} from "./Company";
import {Passport} from "./Passport";

@Entity()
export class Person {

@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@ManyToOne(type => Company)
company: Company;

@ManyToMany(type => Address, address => address.people)
@JoinTable()
addresses: Address[];

@OneToOne(type => Passport, passport => passport.owner)
@JoinColumn()
passport: Passport;

}

0 comments on commit cb17b95

Please sign in to comment.