Skip to content

Latest commit

 

History

History
798 lines (606 loc) · 26.2 KB

README.md

File metadata and controls

798 lines (606 loc) · 26.2 KB

TypeORM

Note: docs are not always up to date because orm is in active development. Samples are more up to date, try to find your questions there. Otherwise create a github issue.

Note: Current version of orm works with typescript >1.9. It means you need to install typescript@next to work with it. If you want to use older version of orm you can try to install [email protected]

What is TypeORM?

TypeORM is an Object Relational Mapper (ORM) for node.js written in Typescript that can be used with Typescript or Javascript (ES5, ES6, ES7). Its goal to always support latest Javascript features and provide features that help you to develop any kind of applications that use database - from small applications with a few tables to large scale enterprise applications. TypeORM helps you to:

  • automatically create in the database table schemas based on your models
  • ability to transparently insert / update / delete to the database your objects
  • map your selections from tables to javascript objects and map table columns to javascript object's properties
  • create one-to-one, many-to-one, one-to-many, many-to-many relations between tables
  • and much more ...

TypeORM uses Data Mapper pattern, unlike all other javascript ORMs that currently exist, which means you can write loosely coupled, scalable, maintainable applications with less problems.

The benefit of using ORM for the programmer is the ability to focus on the business logic and worry about persistence only as a secondary problem.

TypeORM is highly influenced by other ORMs, such as Hibernate and Doctrine.

Installation

  1. Install module:

    npm install typeorm --save

  2. Install required shims:

    npm install es6-shim reflect-metadata --save

And add these lines into some global place in your code, before you starting to use orm:

require("es6-shim");
require("reflect-metadata");

es6-shim is optional for newer versions of node which supports all es6 features. reflect-metadata you definitely need because no platform support this feature yet.

  1. Install database driver:

    3.1. Mysql

     If you want to use ORM with mysql then install its driver:
    
     `npm install mysql --save`
    

    3.2. Postgres (still in development, don't use it yet)

     `npm install pg --save`
    

    Right now only mysql and postgres databases are supported. Feel free to contribute and add support of new drivers.

Example

Lets create a sample application - a photo album.

create Photo entity class

First we create a new file Photo.ts and put a class there:

import {Table} from "typeorm/tables";
import {PrimaryColumn, Column} from "typeorm/columns";

@Table("photo")
export class Photo {
    
    @PrimaryColumn("int", { generated: true })
    id: number;
    
    @Column()
    name: string;
    
    @Column()
    description: string;
    
    @Column()
    filename: string;
    
    @Column()
    isPublished: boolean;
    
}

Here, we are using three decorators:

  • @Table(tableName) - tells ORM to create a new table in the database for this class. We also specified a table name in the database.
  • @PrimaryColumn(columnType, columnOptions) - tells ORM to create a table column for the given class property and make it PRIMARY KEY column. We also set { generated: true } in column options, which makes our primary column an AUTO_INCREMENT.
  • @Column(columnType, columnOptions) - tells ORM to create a table column for the given class property.

connect to the database and register Photo entity class in ORM

Now lets run bootstrap our application and connect to the database. Create app.ts:

import {createConnection, CreateConnectionOptions} from "typeorm";
import {Photo} from "./Photo";

const options: CreateConnectionOptions = {
    driver: "mysql", // specify driver type here. Right now only "mysql" is supported
    connection: {
        host: "localhost", // mysql host
        port: 3306, // mysql port
        username: "root", // mysql database username
        password: "admin", // mysql database password
        database: "test",  // mysql database name
        autoSchemaCreate: true // if set to true, then database schema will be automatically created on each application start
    },
    entities: [Photo] // array of classes you want to create tables for (and work with them in the current connection)
};

createConnection(options).then(connection => {

    // at this point you are connected to the database, and you can
    // perform queries

}).catch(error => console.log("Error during connection to the db: ", error));

Now run your app.ts. ORM will automatically create a photo table in the test database:

+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
| description | varchar(255) |                            |
| filename    | varchar(255) |                            |
| isPublished | boolean      |                            |
+-------------+--------------+----------------------------+

inserting photo into the database

Now lets create a new Photo, and persist it to the database.

createConnection(options).then(connection => {

    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg"
    photo.isPublished = true;
    
    let repository = connection.getRepository(Photo);
    repository.persist(photo).then(photo => {
        console.log("Photo has been persisted to the database.");
        console.log("New photo id is ", photo.id);
    });

});

loading photos from the database

If you want to load photos from the database, you can use repository.find* methods:

// here we load one photo by id:
let photoId = 1;
let repository = connection.getRepository(Photo);
repository.findById(photoId).then(photo => {
    console.log("Photo is loaded: ", photo);
});

// here we load one photo by name
repository.findOne({ name: "Me and Bears" }).then(photo => {
    console.log("Photo is loaded: ", photo);
});

// here we load all published photos
repository.find({ isPublished: true }).then(photos => {
    console.log("Published photos are loaded: ", photos);
});

updating photo in the database

If you want to update in the database a previously loaded photo, you can use repository.persist method:

// change previously loaded photo
photo.name = "Me and Bears and Penguins";
photo.description = "I am near polar bears and penguins";

// call persist method to update a photo
let repository = connection.getRepository(Photo);
repository.persist(photo).then(photo => {
    console.log("Photo is updated in the database: ", photo);
});

removing photo from the database

If you want to remove a photo from the database, you can use repository.remove method:

let repository = connection.getRepository(Photo);
repository.remove(photo).then(() => {
    console.log("Photo has been successfully removed.");
});

creating a one-to-one relation

Lets create a one-to-one relation with another class. Lets create a new class called PhotoMetadata.ts which will contain a PhotoMetadata class which supposed to be contain our Photo's additional meta-information:

import {Table} from "typeorm/tables";
import {PrimaryColumn, Column} from "typeorm/columns";
import {OneToOne, JoinColumn} from "typeorm/relations";

@Table("photo_metadata")
export class PhotoMetadata {
    
    @PrimaryColumn("int", { generated: true })
    id: number;
    
    @Column()
    height: number;
    
    @Column()
    width: number;
    
    @Column()
    comment: string;
    
    @Column()
    compressed: boolean;
    
    @Column()
    orientation: string;
    
    @OneToOne(type => Photo, photo => photo.metadata) // note: we will create metadata property in the Photo class below
    @JoinColumn()
    photo: Photo;
}

Here, we are using a new decorator called @OneToOne. It allows to create one-to-one relations between two entities. @OneToOne decorator accepts two arguments:

  • type => Photo is a function that returns the class of the entity with which relation we want to make our relation.

we are forced to use a function that returns a class, instead of using class directly, because of the language specifics. We can also write it as a () => Photo, but we use type => Photo as convention to increase code readability a bit. type variable itself does not contain anything.

  • photo => photo.metadata is a function that returns a name of the inverse side of the relation. Here we show that metadata property of the Photo class is where we store PhotoMetadata in the Photo class.

you could also instead of passing function that returns a property of the photo simply pass a string to @OneToOne decorator, like "metadata". But we used this function-typed approach to make your refactorings easier.

We also put @JoinColumn decorator, that indicates that this side of the relationship will be owning relationship. Using this decorator is required on owner side of the relationship.

Now lets add inverse side of our relation to the Photo class:

export class Photo {
    /// ... other columns
    
    @OneToOne(type => PhotoMetadata, metadata => metadata.photo)
    metadata: PhotoMetadata;
}

In any relation there are always two sides and only one of them can be owner side. Owner side is called "owner", because it "owns" relation id. In our example PhotoMetadata owns relation because it has a @JoinColumn decorator, thus it will contain photo id. Photo entity does not have @JoinColumn, thus does not have metadata id, and it means that its inverse side of the relationship.

After you run application ORM will create a photo_metadata table:

+-------------+--------------+----------------------------+
|                     photo_metadata                      |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| height      | double       |                            |
| width       | double       |                            |
| comment     | varchar(255) |                            |
| compressed  | boolean      |                            |
| orientation | varchar(255) |                            |
| photo       | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

Don't forget to register PhotoMetadata class for your connection in the ORM:

const options: CreateConnectionOptions = {
    // ... other options
    entities: [Photo, PhotoMetadata]
};

Now lets insert metadata and photo to our database:

createConnection(options).then(connection => {

    // create photo object
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg"
    photo.isPublished = true;

    // create photo metadata object
    let metadata         = new PhotoMetadata();
    metadata.height      = 640;
    metadata.width       = 480;
    metadata.compressed  = true;
    metadata.comment     = "cybershoot";
    metadata.orientation = "portait";
    metadata.photo       = photo; // this way we connect them
    
    // get entity repositories
    let photoRepository = connection.getRepository(Photo);
    let metadataRepository = connection.getRepository(PhotoMetadata);
    
    // first we should persist a photo
    photoRepository.persist(photo).then(photo => {
    
        // photo is saved. Now we need to persist a photo metadata
        return metadataRepository.persist(metadata);
    
    }).then(metadata => {
        // metadata is saved, and relation between metadata and photo is created in the database too
    });

});

using cascade options to automatically save related objects

We can setup cascade options in our relations, in the cases when we want our related object to be persisted whenever other object is saved. Let's change our photo's @OneToOne decorator a bit:

export class Photo {
    /// ... other columns
    
    @OneToOne(type => PhotoMetadata, metadata => metadata.photo, {
        cascadeInsert: true,
        cascadeUpdate: true,
        cascadeRemove: true
    })
    metadata: PhotoMetadata;
}
  • cascadeInsert automatically insert metadata in the relation if it does not exist in its table. This means that we don't need to manually insert a newly created photoMetadata object.
  • cascadeUpdate automatically update metadata in the relation if in this object something is changed
  • cascadeRemove automatically remove metadata from its table if you removed metadata from photo object

Using cascadeInsert allows us not to separately persist photo and separately persist metadata objects now. Now we can simply persist a photo object, and metadata object will persist automatically because of cascade options.

createConnection(options).then(connection => {

    // create photo object
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg"
    photo.isPublished = true;

    // create photo metadata object
    let metadata         = new PhotoMetadata();
    metadata.height      = 640;
    metadata.width       = 480;
    metadata.compressed  = true;
    metadata.comment     = "cybershoot";
    metadata.orientation = "portait";
    metadata.photo       = photo; // this way we connect them
    
    // get repository
    let photoRepository = connection.getRepository(Photo);
    
    // first we should persist a photo
    photoRepository.persist(photo).then(photo => {
        console.log("Photo is saved, photo metadata is saved too.")
    });

});

creating a many-to-one / one-to-many relation

Lets create a many-to-one / one-to-many relation. Lets say a photo has one author, and each author can have many photos. First, lets create a Author class:

import {Table} from "typeorm/tables";
import {PrimaryColumn, Column} from "typeorm/columns";
import {OneToMany} from "typeorm/relations";

@Table("author")
export class Author {
    
    @PrimaryColumn("int", { generated: true })
    id: number;
    
    @Column()
    name: string;
    
    @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below
    photos: Photo[];
}

Now lets add inverse side of our relation to the Photo class:

export class Photo {
    /// ... other columns
    
    @ManyToOne(type => Author, author => author.photos)
    author: Author;
}

In case of many-to-one / one-to-many relation, owner relation is many-to-one. It means that class which uses @ManyToOne will store id of the related object.

After you run application ORM will create author table:

+-------------+--------------+----------------------------+
|                          author                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+

It will also modify photo table - add a new column author and create a foreign key for it:

+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
| description | varchar(255) |                            |
| filename    | varchar(255) |                            |
| isPublished | boolean      |                            |
| author      | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

Don't forget to register Author class for your connection in the ORM:

const options: CreateConnectionOptions = {
    // ... other options
    entities: [Photo, PhotoMetadata, Author]
};

Now lets insert author and photo to our database:

createConnection(options).then(connection => {

    // create a new user
    let author = new Author();
    author.name = "Umed Khudoiberdiev";

    // create photo object
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg"
    photo.author = author;
    
    // get entity repositories
    let photoRepository = connection.getRepository(Photo);
    let authorRepository = connection.getRepository(Author);
    
    // first we should persist our user
    authorRepository.persist(author).then(author => {
    
        // author is saved. Now we need to persist a photo
        return photoRepository.persist(photo);
    
    }).then(photo => {
        // photo is saved, and relation between photo and author is created in the database too
    });

});

creating a many-to-many relation

Lets create a many-to-one / many-to-many relation. Lets say a photo can be in many albums, and multiple can have many photos. Lets create an Album class:

import {Table} from "typeorm/tables";
import {PrimaryColumn, Column} from "typeorm/columns";
import {ManyToMany, JoinTable} from "typeorm/relations";

@Table("album")
export class Album {
    
    @PrimaryColumn("int", { generated: true })
    id: number;
    
    @Column()
    name: string;
    
    @ManyToMany(type => Photo, album => photo.albums, {  // note: we will create "albums" property in the Photo class below
        cascadeInsert: true, // allow to insert a new photo on album save
        cascadeUpdate: true, // allow to update a photo on album save
        cascadeRemove: true  // allow to remove a photo on album remove
    })
    @JoinTable()
    photos: Photo[] = []; // we initialize array for convinience here
}

@JoinTable is required to specify that this is owner side of the relationship.

Now lets add inverse side of our relation to the Photo class:

export class Photo {
    /// ... other columns
    
    @ManyToMany(type => Album, album => album.photos, {
       cascadeInsert: true, // allow to insert a new album on photo save
       cascadeUpdate: true, // allow to update an album on photo save
       cascadeRemove: true  // allow to remove an album on photo remove
    })
    albums: Album[] = []; // we initialize array for convinience here
}

After you run application ORM will create a album_photos_photo_albums junction table:

+-------------+--------------+----------------------------+
|                album_photos_photo_albums                |
+-------------+--------------+----------------------------+
| album_id_1  | int(11)      | FOREIGN KEY                |
| photo_id_2  | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

Don't forget to register Album class for your connection in the ORM:

const options: CreateConnectionOptions = {
    // ... other options
    entities: [Photo, PhotoMetadata, Author, Album]
};

Now lets insert author and photo to our database:

createConnection(options).then(connection => {

    // create a few albums
    let album1 = new Album();
    album1.name = "Bears";
    
    let album2 = new Album();
    album2.name = "Me";

    // create a few photos
    let photo1 = new Photo();
    photo1.name = "Me and Bears";
    photo1.description = "I am near polar bears";
    photo1.filename = "photo-with-bears.jpg"
    
    let photo2 = new Photo();
    photo2.name = "Me and Bears";
    photo2.description = "I am near polar bears";
    photo2.filename = "photo-with-bears.jpg"
    
    // get entity repository
    let photoRepository = connection.getRepository(Photo);
    
    // we only save a photos, albums are persisted automatically because of cascade options
    photoRepository
        .persist(photo1) // first save a first photo
        .then(photo => photoRepository.persist(photo2)) // second save a second photo
        .then(photo => console.log("Both photos have been saved"));
    
});

using FindOptions to customize find queries

Repository.find method allows you to specify findOptions. Using this you can customize your query to perform more complex queries. For example you can do this:

let photoRepository = connection.getRepository(Photo);
photoRepository.find({
    alias: "photo", // this is alias of what you are selecting - photos. You must specify it.
    innerJoinAndSelect: [
        "photo.metadata"
    ],
    leftJoinAndSelect: [
        "photo.albums"
    ],
    where: "photo.isPublished=true AND (photo.name=:photoName OR photo.name=:bearName)",
    orderBy: [{ sort: "photo.id", order: "DESC" }],
    firstResult: 5,
    maxResults: 10,
    parameters: {
        photoName: "My",
        bearName: "Mishka"
    }
}).then(photos => {
    console.log(photos);
});

photoRepository.find will select you all photos that are published and whose name is "My" or "Mishka", it will select results from 5 position (pagination offset), and will select only 10 results (pagination limit). Selection result will be ordered by id in descending order. Photo's albums will be left-joined and photo's metadata will be inner joined.

Learn more about FindOptions here.

using QueryBuilder to build complex queries

You can use QueryBuilder to build even more complex queries. For example you can do this:

let photoRepository = connection.getRepository(Photo);
photoRepository
    .createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it. 
    .innerJoinAndSelect("photo.metadata")
    .leftJoinAndSelect("photo.albums")
    .where("photo.isPublished=true")
    .andWhere("photo.name=:photoName OR photo.name=:bearName")
    .orderBy("photo.id", "DESC")
    .setFirstResult(5)
    .setMaxResults(10)
    .setParameters({ photoName: "My", beaName: "Mishka" })
    .getResults().then(photos => console.log(photos));

This query builder will select you all photos that are published and whose name is "My" or "Mishka", it will select results from 5 position (pagination offset), and will select only 10 results (pagination limit). Selection result will be ordered by id in descending order. Photo's albums will be left-joined and photo's metadata will be inner joined.

Learn more about QueryBuilder here.

using EntityManager to work with any entity

Sometimes you may want to simplify what you are doing and not to create a repository instance for each of your entity to, for example, persist it. In such cases you may want to use EntityManager. These are several methods from EntityManager class:

// create a new user
let author = new Author();
author.name = "Umed Khudoiberdiev";

// create photo metadata
let metadata         = new PhotoMetadata();
metadata.height      = 640;
metadata.width       = 480;
metadata.compressed  = true;
metadata.comment     = "cybershoot";
metadata.orientation = "portait";
metadata.photo       = photo; // this way we connect them

// create a new photo
let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.author = author;
photo.metadata = metadata;

let entityManager = connection.getEntityManager();

// first lets persist entities
entityManager
    .persist(author) // first lets save a new author
    .then(savedAuthor => entityManager.persist(metadata)); // then save a new metadata
    .then(savedMetadata => entityManager.persist(photo)); // and finally save a photo
    .then(savedPhoto => {
        console.log("Everything is saved without using repositories")
     
        // next example is about finding entity and removing it
        entityManager.find(Photo, { isPublished: true }).then(photos => {
            
            // and final example about removing entities
            return Promise.all(photos.map(photo => entityManager.remove(photo)));
        });
        
    });

Learn more about EntityManager here.

Learn more

Samples

Take a look on samples in ./sample for more examples of usages.

Todos

ORM development is in progress. Api can be changed a lot. More documentation and features expected to be soon. Feel free to contribute.