Skip to content

Commit

Permalink
Merge 410a21b into 4e80a9e
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Apr 26, 2020
2 parents 4e80a9e + 410a21b commit cca285d
Show file tree
Hide file tree
Showing 27 changed files with 741 additions and 50 deletions.
2 changes: 1 addition & 1 deletion ROADMAP.md
Expand Up @@ -22,7 +22,7 @@ discuss specifics.
- [x] Single table inheritance ([#33](https://github.com/mikro-orm/mikro-orm/issues/33))
- [x] Allow adding items to not initialized collections
- [x] Use absolute path to entity file in static MetadataStorage keys (fix for 'Multiple property decorators' validation issues)
- [ ] Embedded entities (allow in-lining child entity into parent one with prefixed keys, or maybe as serialized JSON)
- [x] Embedded entities (allow in-lining child entity into parent one with prefixed keys)
- [ ] Association scopes/filters ([hibernate docs](https://docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/filters.html))
- [ ] Support external hooks when using EntitySchema (hooks outside of entity)
- [ ] Support multiple M:N with same properties without manually specifying `pivotTable`
Expand Down
103 changes: 103 additions & 0 deletions docs/docs/embeddables.md
@@ -0,0 +1,103 @@
---
title: Separating Concerns using Embeddables
sidebar_label: Embeddables
---

> Support for embeddables was added in version 4.0
Embeddables are classes which are not entities themselves, but are embedded in
entities and can also be queried. You'll mostly want to use them to reduce
duplication or separating concerns. Value objects such as date range or address
are the primary use case for this feature.

> Embeddables needs to be discovered just like regular entities, don't forget to
> add them to the list of entities when initializing the ORM.
Embeddables can only contain properties with basic `@Property()` mapping.

For the purposes of this tutorial, we will assume that you have a `User` class in
your application and you would like to store an address in the `User` class. We will
model the `Address` class as an embeddable instead of simply adding the respective
columns to the `User` class.

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

@Embedded()
address!: Address;

}

@Embeddable()
export class Address {

@Property()
street!: string;

@Property()
postalCode!: string;

@Property()
city!: string;

@Property()
country!: string;

}
```

> When using ReflectMetadataProvider, you might need to provide the class in decorator options:
> `@Embedded(() => Address)` or `@Embedded({ entity: () => Address })`.
In terms of your database schema, MikroORM will automatically inline all columns from
the `Address` class into the table of the `User` class, just as if you had declared
them directly there.

## Initializing embeddables

In case all fields in the embeddable are nullable, you might want to initialize the
embeddable, to avoid getting a null value instead of the embedded object.

```typescript
@Embedded()
address = new Address();
```

## Column Prefixing

By default, MikroORM names your columns by prefixing them, using the value object name.

Following the example above, your columns would be named as `address_street`,
`address_postal_code`...

You can change this behaviour to meet your needs by changing the `prefix` attribute
in the `@Embedded()` notation.

The following example shows you how to set your prefix to `myPrefix_`:

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

@Embedded({ prefix: 'myPrefix_' })
address!: Address;

}
```

To have MikroORM drop the prefix and use the value object's property name directly,
set `prefix: false`:

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

@Embedded({ entity: () => Address, prefix: false })
address!: Address;

}
```

> This part of documentation is highly inspired by [doctrine tutorial](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/embeddables.html)
> as the behaviour here is pretty much the same.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -55,7 +55,7 @@
"lint": "eslint packages/**/*.ts"
},
"jest": {
"testTimeout": 15000,
"testTimeout": 30000,
"preset": "ts-jest",
"collectCoverage": false,
"collectCoverageFrom": [
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/decorators/Embeddable.ts
@@ -0,0 +1,13 @@
import { AnyEntity } from '../typings';
import { MetadataStorage } from '../metadata';

export function Embeddable(): Function {
return function <T extends { new(...args: any[]): AnyEntity<T> }>(target: T) {
const meta = MetadataStorage.getMetadataFromDecorator(target);
meta.class = target;
meta.name = target.name;
meta.embeddable = true;

return target;
};
}
21 changes: 21 additions & 0 deletions packages/core/src/decorators/Embedded.ts
@@ -0,0 +1,21 @@
import { AnyEntity, EntityProperty } from '../typings';
import { MetadataStorage } from '../metadata';
import { ReferenceType } from '../entity/enums';
import { Utils } from '../utils';

export function Embedded(options: EmbeddedOptions | (() => AnyEntity) = {}): Function {
return function (target: AnyEntity, propertyName: string) {
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);
options = options instanceof Function ? { entity: options } : options;
Utils.defaultValue(options, 'prefix', true);
const property = { name: propertyName, reference: ReferenceType.EMBEDDED } as EntityProperty;
meta.properties[propertyName] = Object.assign(property, options);
};
}

export type EmbeddedOptions = {
entity?: string | (() => AnyEntity);
type?: string;
prefix?: string | boolean;
nullable?: boolean;
};
2 changes: 2 additions & 0 deletions packages/core/src/decorators/index.ts
Expand Up @@ -8,4 +8,6 @@ export * from './Property';
export * from './Enum';
export * from './Indexed';
export * from './Repository';
export * from './Embeddable';
export * from './Embedded';
export * from './hooks';
17 changes: 12 additions & 5 deletions packages/core/src/entity/EntityAssigner.ts
Expand Up @@ -2,7 +2,7 @@ import { inspect } from 'util';
import { Collection } from './Collection';
import { SCALAR_TYPES } from './EntityFactory';
import { EntityManager } from '../EntityManager';
import { EntityData, EntityMetadata, EntityProperty, AnyEntity } from '../typings';
import { AnyEntity, EntityData, EntityMetadata, EntityProperty } from '../typings';
import { Utils } from '../utils';
import { ReferenceType } from './enums';
import { Reference } from './Reference';
Expand All @@ -26,28 +26,35 @@ export class EntityAssigner {
return;
}

if (props[prop]?.inherited || root.discriminatorColumn === prop) {
if (props[prop]?.inherited || root.discriminatorColumn === prop || props[prop]?.embedded) {
return;
}

let value = data[prop as keyof EntityData<T>];

if (props[prop] && props[prop].customType && !Utils.isEntity(data)) {
if (props[prop]?.customType && !Utils.isEntity(data)) {
value = props[prop].customType.convertToJSValue(value, platform);
}

if (props[prop] && [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(props[prop].reference) && Utils.isDefined(value, true) && EntityAssigner.validateEM(em)) {
if ([ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(props[prop]?.reference) && Utils.isDefined(value, true) && EntityAssigner.validateEM(em)) {
return EntityAssigner.assignReference<T>(entity, value, props[prop], em!);
}

if (props[prop] && Utils.isCollection(entity[prop as keyof T], props[prop]) && Array.isArray(value) && EntityAssigner.validateEM(em)) {
return EntityAssigner.assignCollection<T>(entity, entity[prop as keyof T] as unknown as Collection<AnyEntity>, value, props[prop], em!);
}

if (props[prop] && props[prop].reference === ReferenceType.SCALAR && SCALAR_TYPES.includes(props[prop].type) && (props[prop].setter || !props[prop].getter)) {
if (props[prop]?.reference === ReferenceType.SCALAR && SCALAR_TYPES.includes(props[prop].type) && (props[prop].setter || !props[prop].getter)) {
return entity[prop as keyof T] = validator.validateProperty(props[prop], value, entity);
}

if (props[prop]?.reference === ReferenceType.EMBEDDED) {
const Embeddable = props[prop].embeddable;
entity[props[prop].name] = Object.create(Embeddable.prototype);
Utils.merge(entity[prop as keyof T], value);
return;
}

if (options.mergeObjects && Utils.isObject(value)) {
Utils.merge(entity[prop as keyof T], value);
} else if (!props[prop] || !props[prop].getter || props[prop].setter) {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/entity/EntityHelper.ts
Expand Up @@ -36,6 +36,10 @@ export class EntityHelper {
}

static decorate<T extends AnyEntity<T>>(meta: EntityMetadata<T>, em: EntityManager): void {
if (meta.embeddable) {
return;
}

const pk = meta.properties[meta.primaryKeys[0]];

if (pk.name === '_id') {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/entity/enums.ts
Expand Up @@ -4,6 +4,7 @@ export enum ReferenceType {
ONE_TO_MANY = '1:m',
MANY_TO_ONE = 'm:1',
MANY_TO_MANY = 'm:n',
EMBEDDED = 'embedded',
}

export enum Cascade {
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/hydration/Hydrator.ts
Expand Up @@ -15,11 +15,15 @@ export abstract class Hydrator {
meta = metadata.get(entity.constructor.name);
}

for (const prop of Object.values<EntityProperty>(meta.properties).filter(prop => !prop.inherited && root.discriminatorColumn !== prop.name)) {
this.hydrateProperty(entity, prop, data[prop.name], newEntity);
const props = Object.values<EntityProperty>(meta.properties).filter(prop => {
return !prop.inherited && root.discriminatorColumn !== prop.name && !prop.embedded;
});

for (const prop of props) {
this.hydrateProperty(entity, prop, data, newEntity);
}
}

protected abstract hydrateProperty<T extends AnyEntity<T>>(entity: T, prop: EntityProperty, value: EntityData<T>[any], newEntity: boolean): void;
protected abstract hydrateProperty<T extends AnyEntity<T>>(entity: T, prop: EntityProperty, value: EntityData<T>, newEntity: boolean): void;

}
25 changes: 19 additions & 6 deletions packages/core/src/hydration/ObjectHydrator.ts
@@ -1,19 +1,21 @@
import { AnyEntity, EntityData, EntityProperty, Primary } from '../typings';
import { AnyEntity, Dictionary, EntityData, EntityProperty, Primary } from '../typings';
import { Hydrator } from './Hydrator';
import { Collection, EntityAssigner, ReferenceType, wrap } from '../entity';
import { Utils } from '../utils';

export class ObjectHydrator extends Hydrator {

protected hydrateProperty<T extends AnyEntity<T>>(entity: T, prop: EntityProperty, value: any, newEntity: boolean): void {
protected hydrateProperty<T extends AnyEntity<T>>(entity: T, prop: EntityProperty, data: EntityData<T>, newEntity: boolean): void {
if (prop.reference === ReferenceType.MANY_TO_ONE || prop.reference === ReferenceType.ONE_TO_ONE) {
this.hydrateManyToOne(value, entity, prop);
this.hydrateManyToOne(data[prop.name], entity, prop);
} else if (prop.reference === ReferenceType.ONE_TO_MANY) {
this.hydrateOneToMany(entity, prop, value, newEntity);
this.hydrateOneToMany(entity, prop, data[prop.name], newEntity);
} else if (prop.reference === ReferenceType.MANY_TO_MANY) {
this.hydrateManyToMany(entity, prop, value, newEntity);
this.hydrateManyToMany(entity, prop, data[prop.name], newEntity);
} else if (prop.reference === ReferenceType.EMBEDDED) {
this.hydrateEmbeddable(entity, prop, data);
} else { // ReferenceType.SCALAR
this.hydrateScalar(entity, prop, value);
this.hydrateScalar(entity, prop, data[prop.name]);
}
}

Expand All @@ -33,6 +35,17 @@ export class ObjectHydrator extends Hydrator {
entity[prop.name as keyof T] = value;
}

private hydrateEmbeddable<T extends AnyEntity<T>>(entity: T, prop: EntityProperty, data: EntityData<T>): void {
const value: Dictionary = {};

Object.values<EntityProperty>(wrap(entity).__meta.properties).filter(p => p.embedded?.[0] === prop.name).forEach(childProp => {
value[childProp.embedded![1]] = data[childProp.name];
});

entity[prop.name] = Object.create(prop.embeddable.prototype);
Object.keys(value).forEach(k => entity[prop.name][k] = value[k]);
}

private hydrateManyToMany<T extends AnyEntity<T>>(entity: T, prop: EntityProperty, value: any, newEntity: boolean): void {
if (prop.owner) {
return this.hydrateManyToManyOwner(entity, prop, value, newEntity);
Expand Down
22 changes: 18 additions & 4 deletions packages/core/src/metadata/EntitySchema.ts
@@ -1,6 +1,6 @@
import { AnyEntity, Constructor, Dictionary, EntityMetadata, EntityName, EntityProperty } from '../typings';
import {
EnumOptions, IndexOptions, ManyToManyOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, PrimaryKeyOptions, PropertyOptions,
EmbeddedOptions, EnumOptions, IndexOptions, ManyToManyOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, PrimaryKeyOptions, PropertyOptions,
SerializedPrimaryKeyOptions, UniqueOptions,
} from '../decorators';
import { Cascade, Collection, EntityRepository, ReferenceType } from '../entity';
Expand All @@ -15,6 +15,7 @@ type Property<T> =
| ({ reference: ReferenceType.ONE_TO_ONE | '1:1' } & TypeDef<T> & OneToOneOptions<T>)
| ({ reference: ReferenceType.ONE_TO_MANY | '1:m' } & TypeDef<T> & OneToManyOptions<T>)
| ({ reference: ReferenceType.MANY_TO_MANY | 'm:n' } & TypeDef<T> & ManyToManyOptions<T>)
| ({ reference: ReferenceType.EMBEDDED | 'embedded' } & TypeDef<T> & EmbeddedOptions & PropertyOptions)
| ({ enum: true } & EnumOptions)
| (TypeDef<T> & PropertyOptions);
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
Expand All @@ -38,7 +39,7 @@ export class EntitySchema<T extends AnyEntity<T> = AnyEntity, U extends AnyEntit
meta.tableName = meta.collection;
}

Object.assign(this._meta, { className: meta.name, properties: {}, hooks: {}, indexes: [], uniques: [] }, meta);
Object.assign(this._meta, { className: meta.name, properties: {}, hooks: {}, primaryKeys: [], indexes: [], uniques: [] }, meta);
this.internal = internal;
}

Expand Down Expand Up @@ -91,6 +92,16 @@ export class EntitySchema<T extends AnyEntity<T> = AnyEntity, U extends AnyEntit
this.addProperty(name, type, options);
}

addEmbedded<K = object>(name: string & keyof T, options: EmbeddedOptions): void {
Utils.defaultValue(options, 'prefix', true);
this._meta.properties[name] = {
name,
type: this.normalizeType(options),
reference: ReferenceType.EMBEDDED,
...options,
} as EntityProperty<T>;
}

addManyToOne<K = object>(name: string & keyof T, type: TypeType, options: ManyToOneOptions<K>): void {
const prop = { reference: ReferenceType.MANY_TO_ONE, cascade: [Cascade.PERSIST, Cascade.MERGE], ...options };
Utils.defaultValue(prop, 'nullable', prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL));
Expand Down Expand Up @@ -197,14 +208,17 @@ export class EntitySchema<T extends AnyEntity<T> = AnyEntity, U extends AnyEntit
this.addOneToOne(name as keyof T & string, options.type as string, options);
break;
case ReferenceType.ONE_TO_MANY:
this.addOneToMany(name as keyof T & string, options.type as string, options);
this.addOneToMany(name as keyof T & string, options.type as string, options as OneToManyOptions<T>);
break;
case ReferenceType.MANY_TO_ONE:
this.addManyToOne(name as keyof T & string, options.type as string, options);
break;
case ReferenceType.MANY_TO_MANY:
this.addManyToMany(name as keyof T & string, options.type as string, options);
break;
case ReferenceType.EMBEDDED:
this.addEmbedded(name as keyof T & string, options as EmbeddedOptions);
break;
default:
if ((options as EntityProperty).enum) {
this.addEnum(name as keyof T & string, options.type as string, options);
Expand Down Expand Up @@ -241,7 +255,7 @@ export class EntitySchema<T extends AnyEntity<T> = AnyEntity, U extends AnyEntit
}
}

private normalizeType(options: PropertyOptions | EntityProperty, type: string | any | Constructor<Type>) {
private normalizeType(options: PropertyOptions | EntityProperty, type?: string | any | Constructor<Type>) {
if ('entity' in options) {
if (Utils.isString(options.entity)) {
type = options.type = options.entity;
Expand Down

0 comments on commit cca285d

Please sign in to comment.