Skip to content

Commit

Permalink
Merge 7706b5b into d5db6c8
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Mar 21, 2020
2 parents d5db6c8 + 7706b5b commit c36ec10
Show file tree
Hide file tree
Showing 77 changed files with 1,965 additions and 442 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -148,6 +148,7 @@ There is also auto-generated [CHANGELOG.md](CHANGELOG.md) file based on commit m
- [Unit of Work](https://mikro-orm.io/unit-of-work/)
- [Transactions](https://mikro-orm.io/transactions/)
- [Cascading persist and remove](https://mikro-orm.io/cascading/)
- [Composite and Foreign Keys as Primary Key](https://mikro-orm.io/composite-keys/)
- [Using `QueryBuilder`](https://mikro-orm.io/query-builder/)
- [Preloading Deeply Nested Structures via populate](https://mikro-orm.io/nested-populate/)
- [Property Validation](https://mikro-orm.io/property-validation/)
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Expand Up @@ -28,3 +28,4 @@ discuss specifics.
- Split into multiple packages (core, driver packages, TS support, SQL support, CLI)
- Drop default value for db `type`
- Remove `autoFlush` option
- Remove `IdEntity/UuidEntity/MongoEntity` interfaces
247 changes: 247 additions & 0 deletions docs/docs/composite-keys.md
@@ -0,0 +1,247 @@
---
title: Composite and Foreign Keys as Primary Key
sidebar_label: Composite Primary Keys
---

> Support for composite keys was added in version 3.5
MikroORM supports composite primary keys natively. Composite keys are a very powerful
relational database concept and we took good care to make sure MikroORM supports as
many of the composite primary key use-cases. MikroORM supports composite keys of primitive
data-types as well as foreign keys as primary keys. You can also use your composite key
entities in relationships.

This section shows how the semantics of composite primary keys work and how they map
to the database.

## General Considerations

ID fields have to have their values set before you call `em.persist(entity)`.

## Primitive Types only

Suppose you want to create a database of cars and use the model-name and year of
production as primary keys:

```typescript
@Entity()
export class Car {

@PrimaryKey()
name: string;

@PrimaryKey()
year: number;

[PrimaryKeyType]: [string, number]; // this is needed for proper type checks in `FilterQuery`

constructor(name: string, year: number) {
this.name = name;
this.year = year;
}

}
```

Now you can use this entity:

```typescript
const car = new Car('Audi A8', 2010);
await em.persistAndFlush(car);
```

And for querying you need to provide all primary keys in the condition or an array of
primary keys in the same order as the keys were defined:

```typescript
const audi1 = await em.findOneOrFail(Car, { name: 'Audi A8', year: 2010 });
const audi2 = await em.findOneOrFail(Car, ['Audi A8', 2010]);
```

You can also use this entity in associations. MikroORM will then generate two foreign
keys one for name and to year to the related entities.

This example shows how you can nicely solve the requirement for existing values before em.persist(): By adding them as mandatory values for the constructor.

## Identity through foreign Entities

There are tons of use-cases where the identity of an Entity should be determined by
the entity of one or many parent entities.

- Dynamic Attributes of an Entity (for example Article). Each Article has many attributes with primary key 'article_id' and 'attribute_name'.
- Address object of a Person, the primary key of the address is 'user_id'. This is not a case of a composite primary key, but the identity is derived through a foreign entity and a foreign key.
- Join Tables with metadata can be modelled as Entity, for example connections between two articles with a little description and a score.

The semantics of mapping identity through foreign entities are easy:

- Only allowed on Many-To-One or One-To-One associations.
- Plug an @Id annotation onto every association.
- Set an attribute association-key with the field name of the association in XML.

## Use-Case 1: Dynamic Attributes

We keep up the example of an Article with arbitrary attributes, the mapping looks like this:

```typescript
@Entity()
export class Article {

@PrimaryKey()
id!: number;

@Property()
title!: string;

@OneToMany(() => ArticleAttribute, attr => attr.article, { cascade: Cascade.ALL })
attributes = new Collection<ArticleAttribute>(this);

}

@Entity()
export class ArticleAttribute {

@ManyToOne()
article: Article;

@PrimaryKey()
attribute: string;

@Property()
value!: string;

[PrimaryKeyType]: [number, number]; // this is needed for proper type checks in `FilterQuery`

constructor(name: string, value: string, article: Article) {
this.attribute = name;
this.value = value;
this.article = article;
}

}
```

## Use-Case 2: Simple Derived Identity

Sometimes you have the requirement that two objects are related by a One-To-One association and that the dependent class should re-use the primary key of the class it depends on. One good example for this is a user-address relationship:

```typescript
@Entity()
export class User {

@PrimaryKey()
id!: number;

@OneToOne(() => Address2, address => address.author, { cascade: [Cascade.ALL] })
address?: Address2;

}

@Entity()
export class Address {

@OneToOne()
user!: User;

}
```

## Use-Case 3: Join-Table with Metadata

In the classic order product shop example there is the concept of the order item which
contains references to order and product and additional data such as the amount of products
purchased and maybe even the current price.

```typescript
@Entity()
export class Order {

@PrimaryKey()
id!: number;

@ManyToOne()
customer: Customer;

@OneToMany(() => OrderItem, item => item.order)
items = new Collection<OrderItem>(this);

@Property()
paid = false;

@Property()
shipped = false;

@Property()
created = new Date();

constructor(customer: Customer) {
this.customer = customer;
}

}

@Entity()
export class Product {

@PrimaryKey()
id!: number;

@Property()
name!: string;

@Property()
currentPrice!: number;

}

@Entity()
export class OrderItem {

@ManyToOne({ primary: true })
order: Order;

@ManyToOne({ primary: true })
product: Product;

@Property()
amount = 1;

@Property()
offeredPrice: number;

constructor(order: Order, product: Product, amount = 1) {
this.order = order;
this.product = product;
this.offeredPrice = product.currentPrice;
}

}
```

## Using QueryBuilder with composite keys

Internally composite keys are represented as tuples, containing all the values in the
same order as the primary keys were defined.

```typescript
const qb1 = em.createQueryBuilder(CarOwner);
qb1.select('*').where({ car: { name: 'Audi A8', year: 2010 } });
console.log(qb1.getQuery()); // select `e0`.* from `car_owner` as `e0` where `e0`.`name` = ? and `e0`.`year` = ?

const qb2 = em.createQueryBuilder(CarOwner);
qb2.select('*').where({ car: ['Audi A8', 2010] });
console.log(qb2.getQuery()); // 'select `e0`.* from `car_owner` as `e0` where (`e0`.`car_name`, `e0`.`car_year`) = (?, ?)'

const qb3 = em.createQueryBuilder(CarOwner);
qb3.select('*').where({ car: [['Audi A8', 2010]] });
console.log(qb3.getQuery()); // 'select `e0`.* from `car_owner` as `e0` where (`e0`.`car_name`, `e0`.`car_year`) in ((?, ?))'
```

This also applies when you want to get a reference to entity with composite key:

```typescript
const ref = em.getReference(Car, ['Audi A8', 2010]);
console.log(ref instanceof Car); // true
```

> This part of documentation is highly inspired by [doctrine tutorial](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html)
> as the behaviour here is pretty much the same.
1 change: 1 addition & 0 deletions docs/sidebars.js
Expand Up @@ -15,6 +15,7 @@ module.exports = {
'unit-of-work',
'transactions',
'cascading',
'composite-keys',
'deployment',
'decorators',
],
Expand Down
20 changes: 15 additions & 5 deletions lib/EntityManager.ts
Expand Up @@ -340,27 +340,37 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
/**
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
*/
getReference<T extends AnyEntity<T>, PK extends keyof T>(entityName: EntityName<T>, id: Primary<T>, wrapped: true): IdentifiedReference<T, PK>;
getReference<T extends AnyEntity<T>, PK extends keyof T>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[], wrapped: true): IdentifiedReference<T, PK>;

/**
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
*/
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T>): T;
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[]): T;

/**
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
*/
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T>, wrapped: false): T;
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[], wrapped: false): T;

/**
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
*/
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T>, wrapped: boolean): T | Reference<T>;
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[], wrapped: boolean): T | Reference<T>;

/**
* Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
*/
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T>, wrapped = false): T | Reference<T> {
getReference<T extends AnyEntity<T>>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[], wrapped = false): T | Reference<T> {
const meta = this.metadata.get(Utils.className(entityName));

if (Utils.isPrimaryKey(id)) {
if (meta.compositePK) {
throw ValidationError.invalidCompositeIdentifier(meta);
}

id = [id];
}

const entity = this.getEntityFactory().createReference<T>(entityName, id);
this.getUnitOfWork().merge(entity, [entity], false);

Expand Down
2 changes: 2 additions & 0 deletions lib/decorators/ManyToMany.ts
Expand Up @@ -29,6 +29,8 @@ export interface ManyToManyOptions<T extends AnyEntity<T>> extends ReferenceOpti
fixedOrderColumn?: string;
pivotTable?: string;
joinColumn?: string;
joinColumns?: string[];
inverseJoinColumn?: string;
inverseJoinColumns?: string[];
referenceColumnName?: string;
}
2 changes: 2 additions & 0 deletions lib/decorators/ManyToOne.ts
Expand Up @@ -27,6 +27,8 @@ export interface ManyToOneOptions<T extends AnyEntity<T>> extends ReferenceOptio
inversedBy?: (string & keyof T) | ((e: T) => any);
wrappedReference?: boolean;
primary?: boolean;
joinColumn?: string;
joinColumns?: string[];
onDelete?: string;
onUpdateIntegrity?: string;
}
2 changes: 2 additions & 0 deletions lib/decorators/OneToMany.ts
Expand Up @@ -42,7 +42,9 @@ export type OneToManyOptions<T extends AnyEntity<T>> = ReferenceOptions<T> & {
orphanRemoval?: boolean;
orderBy?: { [field: string]: QueryOrder };
joinColumn?: string;
joinColumns?: string[];
inverseJoinColumn?: string;
inverseJoinColumns?: string[];
referenceColumnName?: string;
mappedBy?: (string & keyof T) | ((e: T) => any);
};
1 change: 1 addition & 0 deletions lib/decorators/Property.ts
Expand Up @@ -28,6 +28,7 @@ export function Property(options: PropertyOptions = {}): Function {
export type PropertyOptions = {
name?: string;
fieldName?: string;
fieldNames?: string[];
columnType?: string;
type?: any;
length?: any;
Expand Down

0 comments on commit c36ec10

Please sign in to comment.