Kononnable / typeorm-model-generator

Generates models for TypeORM from existing database.
MIT License
1.51k stars 281 forks source link

[MySQL] OneToOne not creating child's primary column #217

Closed boenrobot closed 4 years ago

boenrobot commented 4 years ago

I have two tables - user and user_details defined as:

CREATE TABLE IF NOT EXISTS `users` (
  `user_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `username` VARCHAR(255) NOT NULL,
  `email_hmac` VARCHAR(64) NOT NULL,
  `is_login_disabled` TINYINT(1) NOT NULL DEFAULT 1,
  `is_inherited` TINYINT(1) NOT NULL DEFAULT 0,
  PRIMARY KEY (`user_id`),
  UNIQUE INDEX `username_UNIQUE` (`username` ASC) VISIBLE)
ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS `user_details` (
  `user_id` INT UNSIGNED NOT NULL,
  `password` VARCHAR(255) NOT NULL,
  `email` VARCHAR(4096) NOT NULL,
  `skype_account` VARCHAR(255) NOT NULL,
  PRIMARY KEY (`user_id`),
  CONSTRAINT `fk_user_details_users1`
    FOREIGN KEY (`user_id`)
    REFERENCES `users` (`user_id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
ENGINE = InnoDB;

Important thing to note is that user_details.user_id is a non-nullable non-generated column that is the primary key for user_details, and is a foreign key that references users.user_id.

With the following options as part of .torm-config:

{
        "convertCaseFile": "param",
        "convertCaseEntity": "pascal",
        "convertCaseProperty": "camel",
        "propertyVisibility": "public",
        "lazy": true,
        "skipSchema": true,
        "activeRecord": false,
        "generateConstructor": true,
        "relationIds": true
}

The generated code is users.ts

import {BaseEntity,Column,Entity,Index,JoinColumn,JoinTable,ManyToMany,ManyToOne,OneToMany,OneToOne,PrimaryColumn,PrimaryGeneratedColumn,RelationId} from "typeorm";
import {UserDetails} from "./user-details";

@Entity("users")
@Index("username_UNIQUE",["username",],{unique:true})
export class Users {

    @PrimaryGeneratedColumn({
        type:"int", 
        unsigned: true,
        name:"user_id"
        })
    public userId:number;

    @Column("datetime",{ 
        nullable:false,
        default: () => "CURRENT_TIMESTAMP",
        name:"created_at"
        })
    public createdAt:Date;

    @Column("varchar",{ 
        nullable:false,
        unique: true,
        name:"username"
        })
    public username:string;

    @Column("varchar",{ 
        nullable:false,
        length:64,
        name:"email_hmac"
        })
    public emailHmac:string;

    @Column("tinyint",{ 
        nullable:false,
        width:1,
        default: () => "'1'",
        name:"is_login_disabled"
        })
    public isLoginDisabled:boolean;

    @Column("tinyint",{ 
        nullable:false,
        width:1,
        default: () => "'0'",
        name:"is_inherited"
        })
    public isInherited:boolean;

    @OneToOne(()=>UserDetails, (userDetails: UserDetails)=>userDetails.user,{ onDelete: 'CASCADE' ,onUpdate: 'CASCADE' })
    public userDetails:Promise<UserDetails | null>;

    public constructor(init?: Partial<Users>) {
        Object.assign(this, init);
    }
}

user-details.ts

import {BaseEntity,Column,Entity,Index,JoinColumn,JoinTable,ManyToMany,ManyToOne,OneToMany,OneToOne,PrimaryColumn,PrimaryGeneratedColumn,RelationId} from "typeorm";
import {Users} from "./users";

@Entity("user_details")
export class UserDetails {

    @OneToOne(()=>Users, (users: Users)=>users.userDetails,{ primary:true, nullable:false,onDelete: 'CASCADE',onUpdate: 'CASCADE' })
    @JoinColumn({ name:'user_id'})
    public user:Promise<Users | null>;

    @RelationId((userDetails: UserDetails) => userDetails.user)
    public userId: Promise<number>;

    @Column("varchar",{ 
        nullable:false,
        name:"password"
        })
    public password:string;

    @Column("varchar",{ 
        nullable:false,
        length:4096,
        name:"email"
        })
    public email:string;

    @Column("varchar",{ 
        nullable:false,
        name:"skype_account"
        })
    public skypeAccount:string;

    public constructor(init?: Partial<UserDetails>) {
        Object.assign(this, init);
    }
}

The problem is that the generated userId in UserDetails is a @RelationId for the users table, but not a @Column, meaning one can't insert into that table with an entity (only with a raw query). Furthermore, with lazy relations enabled, simply adding @Column isn't quote going to cut it, as typeorm doesn't resolve the promise before using the ID.

Disabling relation IDs eliminates userId from UserDetails entirely, rather than create a @Column in its place.

I'm only just starting to work with TypeORM though, so I'm not sure what should the generator do... special casing such columns (somehow) to generate a single property decorated with both @Column and @RelationId AND making the property non-lazy regardless of lazy setting works... except that whenever one inserts/updates a UserDetails entity, there's also a (seemingly?) unnecesarry select for the related User from the users table.

Adding a second property decorated with @Column, while keeping one only with @RelationId would solve the above, but has its own problems, in particular, naming... Relation IDs would have to be subject to a naming strategy, in a separate method, and by default be named after the column with some prefix/suffix, to ensure it doesn't collide.

Or perhaps OneToOne relations should be special cased, so that a @Column is generated with the ID regardless of @RelationId setting, and @RelationId is never to be generated for those (since the ID would be known by the @Column).

Kononnable commented 4 years ago

The problem is that the generated userId in UserDetails is a @RelationId for the users table, but not a @Column, meaning one can't insert into that table with an entity (only with a raw query). Furthermore, with lazy relations enabled, simply adding @Column isn't quote going to cut it, as typeorm doesn't resolve the promise before using the ID.

You can still insert a row without using raw query. Take a look at this snippet:

    const user = new Users();
    user.username = 'username';
    user.emailHmac = 'emailHmac'
    const userRepo = typeorm.getRepository(Users);
    await userRepo.save(user);

    const details = new UserDetails();
    details.email = "email"
    details.password = 'password'
    details.skypeAccount = 'skypeAccount'
    details.user = Promise.resolve(user);
    const userDetailsRepo = typeorm.getRepository(UserDetails);
    const response = await userDetailsRepo.save(details);

Due to how typeorm lazy relation works you have to put user object into promise box.

Side-note: In typeorm@0.3.0 having one entity field for relation and primary key isn't allowed. Proper way is to generate two entity fields(@Column and @OneToOne) and mark column field as primary - such code is generated on @next branch (#222).

boenrobot commented 4 years ago

I tried doing it in that way initially, but it didn't worked. TypeORM attempted to insert into user_details without the user_id column.

What you're saying is the proper way is what happens in #218 as well.

I'll try #222.

Kononnable commented 4 years ago

I've tested this code snippet and it worked correctly (typeorm@0.2.18). Maybe you didn't await userRepo.save(user); before trying to save details?