mikro-orm / mikro-orm

TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.
https://mikro-orm.io
MIT License
7.68k stars 535 forks source link

Different ways to describe schema #283

Closed matiux closed 4 years ago

matiux commented 4 years ago

I'm working on a typescript project with the nestjs framework. I also come from PHP and Doctrine orm. Doctrine has the ability to works with different ways to describe a schema, annotation, yaml and xml. Yaml will be deprecated soon (even though I've always used it and I'm sorry for this choice), annotation, in my opinion is a bad way because binds infrastuctural concepts to domain concepts (I love DDD and exagonal architecture).

So the remaining alternative is xml.

Is there a way with Mikro-orm to handle this case? I'd like to write my aggregate, and define a schema in a different file (xml, json, yaml ecc), without using typescript decorators.

The alternative I have in mind is to create my Aggregate (more simply an entity) as a domain concept, and extend this entity in the infrastructural context to decorate the properties with Mikro-orm "annotation". But I have to try yet.

What do you tinkh about this?

B4nan commented 4 years ago

Your best bet is to implement custom MetadataProvider. Take a look at how JavascriptMetadaProvider is implemented, it should be pretty similar.

I will be happy to adjust how the discovery works to support additional usage like what you just described, but you might be fine with what is there already.

whocaresk commented 4 years ago

I'm also interested in that feature, looking forward to throw away buggy typeorm from my project and replace it with this ORM. Some examples of configuring schemas without classes and annotations in typescript would be really helpful. I wasn't able to find a word about custom MetadataProvider in the docs, only references to javascript configuration (need to dig deeper, but maybe there isn't really difference between ts and js config).

I also would mention that implementation of schemas in typeorm are pretty good for endpoint user (wish it works as it looks, lol), maybe should introduce something similar?

Sidenotes: Is it possible to map whole object from db to single property of entity? I have something similar to this, and later data accessed from that property through getters/setters

B4nan commented 4 years ago

Currently there is not much said about custom metadata providers in the docs, I will try to add a section about that to the metadata providers page later.

Take a look at MetadataDiscovery class, that is the only place where the MetadataProvider instance is used:

https://github.com/mikro-orm/mikro-orm/blob/96b2cad350063e1cbf3f2485a762fba83c6f448c/lib/metadata/MetadataDiscovery.ts#L123 https://github.com/mikro-orm/mikro-orm/blob/96b2cad350063e1cbf3f2485a762fba83c6f448c/lib/metadata/MetadataDiscovery.ts#L129

The MetadataProvider abstract class already implement loadFromCache method, so the only thing you need to implement abstract async loadEntityMetadata(meta: EntityMetadata, name: string): Promise<void> method.

Here is a code of that method in JavascriptMetadaProvider I was referring to:

async loadEntityMetadata(meta: EntityMetadata, name: string): Promise<void> {
  const schema = this.getSchema(meta);
  Object.entries(schema.properties).forEach(([name, prop]) => {
    if (Utils.isString(prop)) {
      schema.properties[name] = { type: prop };
    }
  });

  Utils.merge(meta, schema);
  Object.entries(meta.properties).forEach(([name, prop]) => {
    this.initProperty(prop, name);
  });
}

// here we fill some default values that are required for the property definition
private initProperty(prop: EntityProperty, propName: string): void {
  prop.name = propName;

  if (typeof prop.reference === 'undefined') {
    prop.reference = ReferenceType.SCALAR;
  }

  if (prop.reference !== ReferenceType.SCALAR && typeof prop.cascade === 'undefined') {
    prop.cascade = [Cascade.PERSIST, Cascade.MERGE];
  }
}

// require the entity file and read exported `schema` variable
private getSchema(meta: EntityMetadata) {
  const path = Utils.absolutePath(meta.path, this.config.get('baseDir'));
  const { schema } = require(path);

  return schema;
}

The naming here is bad, you can use it with typescript too. It is a simple provider that reads schema property (of type EntityMetadata) exported from the entity file.

Here is how an entity could look like in typescript when you use the JavascriptMetadaProvider:

export class FooBar implements IdEntity<FooBar> {
  id: number;
  name?: string;
  registered: boolean;
  version: number;
}

export const schema = {
  name: 'FooBar',
  properties: {
    id: {
      type: 'number',
      primary: true,
    },
    name: {
      type: 'string',
      nullable: true,
      length: 50,
    },
    name: {
      type: 'boolean',
      default: true,
    },
    version: {
      version: true,
      type: 'number',
    },
  },
  path: __filename,
};
export const entity = FooBar;

I would like to add some better/type-safe way to do this, but not a high priority for now. With custom metadata providers you can achieve anything I believe.

Is it possible to map whole object from db to single property of entity? I have something similar to this, and later data accessed from that property through getters/setters

You can have JSON(b) column with pretty much anything, it should work in all drivers. You can also define custom type in latest RC: https://mikro-orm.io/docs/custom-types/

B4nan commented 4 years ago

I also would mention that implementation of schemas in typeorm are pretty good for endpoint user (wish it works as it looks, lol), maybe should introduce something similar?

Looking at how typeorm does this (https://typeorm.io/#/separating-entity-definition, you mean this, right?), I would like to support something like that too at some point, probably with a bit more OO API. I was planning to do internal refactoring to convert the EntityMetadata and EntityProperty interfaces to objects anyway so it can be used with vanilla JS to simplify entity definition.