typeorm / typeorm

ORM for TypeScript and JavaScript. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.
http://typeorm.io
MIT License
34.51k stars 6.34k forks source link

SubjectWithoutIdentifierError on attempted remove() of entity #4544

Open regan-karlewicz opened 5 years ago

regan-karlewicz commented 5 years ago

Issue type:

[ ] question [x] bug report [ ] feature request [ ] documentation issue

Database system/driver:

[ ] cordova [ ] mongodb [ ] mssql [ ] mysql / mariadb [ ] oracle [x] postgres [ ] cockroachdb [ ] sqlite [ ] sqljs [ ] react-native [ ] expo

TypeORM version:

[x] latest (0.2.18) [ ] @next [ ] 0.x.x (or put your version here)

Steps to reproduce or a small repository showing the problem:

I have three entities involved in this operation, User, Org, and UserOrg (a join table between User and Org) I had to create a 3rd entity for the join table between User and Org because the @ManyToMany decorator does not support adding extra attributes (in this case, the extra attribute needed was isAdmin)

The problem arises when I try to call .remove() on the UserOrg entity after finding an instance with the query builder. See details below:

Org:

@Entity()
export class Org extends BaseEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public name: string;

  @OneToMany(_type => UserOrg, userOrg => userOrg.org)
  public users: Promise<UserOrg[]>;
}

User:

@Entity({ name: 'dcUser' })
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public username: string;

  @OneToMany(_type => UserOrg, userOrg => userOrg.user)
  public orgs: Promise<UserOrg[]>;

UserOrg:

@Entity("org_users_dc_user")
export class UserOrg extends BaseEntity {
  @ManyToOne(_type => User, user => user.orgs, { primary: true, eager: true, onDelete: 'CASCADE' })
  public user: User;

  @ManyToOne(_type => Org, org => org.users, { primary: true, eager: true, onDelete: 'CASCADE' })
  public org: Org;

  @Column()
  public isAdmin: boolean;
}

The error:

  1. I query the UserOrg entity:

    const userOrg = await createQueryBuilder('userOrg')
      .innerJoinAndSelect("userOrg.user", "user", "user.username = :username", { username })
      .innerJoin("userOrg.org", "org", "org.id = :orgId", { orgId })
      .getOne();
  2. The query successfully returns a row in the UserOrg table. I call remove on the UserOrg like await userOrg.remove()

  3. Error is thrown by the remove method:

    {"name":"SubjectWithoutIdentifierError","message":"Internal error. Subject UserOrg must have an identifier to perform operation. Please report a github issue if you face this error."}

I hope this is enough information to reproduce this issue.

3615 looks like a similar issue, yet does not offer remedy

4514 looks similar also, but suggests that adding onDelete: 'CASCADE' will fix the issue. I tried with no luck (as seen in the above entities)

wolfpack94 commented 5 years ago

Facing the same issue

derbenoo commented 5 years ago

Faced the same issue, after some debugging I found a solution! The problem is that the IDs of the associations are not correctly set after loading the entity, we can easily fix this manually inside an @AfterLoad function though:

UserOrg

@Entity("org_users_dc_user")
export class UserOrg extends BaseEntity {
  @ManyToOne(_type => User, user => user.orgs, { primary: true, eager: true, onDelete: 'CASCADE' })
  public user: User;

  @RelationId((userOrg: UserOrg) => userOrg.user) // Add user relationId
  public userId: number;

  @ManyToOne(_type => Org, org => org.users, { primary: true, eager: true, onDelete: 'CASCADE' })
  public org: Org;

  @RelationId((userOrg: UserOrg) => userOrg.org) // Add org relationId
  public orgId: number;

  @Column()
  public isAdmin: boolean;

  @AfterLoad() // Custom afterLoad method to manually set the associations correctly
  setAssociations() {
    if (!this.org && this.orgId) {
      this.org = { id: this.orgId } as Org;
    }

    if (!this.user && this.userId) {
      this.user = { id: this.userId } as User;
    }
  }
}

Hope this solves your problem @regan-karlewicz @wolfpack94 !

Cheers, derbenoo

MSafter commented 4 years ago

Hi, I was facing the same issue - maybe good to know: The .remove awaits a db entry (or only id) so you can't use a condition like in .delete.

And more important - as described in this page it says, that SubjectWithoutIdentifierError is to avoid deleting the whole database.

Thrown when operation is going to be executed on a subject without identifier. This error should never be thrown, however it still presents to prevent user from updation or removing the whole table. If this error occurs still, it most probably is an ORM internal problem which must be reported and fixed.

leonardossz commented 4 years ago

I had the same situation.

@derbenoo gave me the hint of the problem. I have a table very similar to his UserOrg and I just had to change the query to fetch the missing data.

Taking his example:

const userOrg = await createQueryBuilder('userOrg')
      .innerJoinAndSelect("userOrg.user", "user", "user.username = :username", { username })
      .innerJoin("userOrg.org", "org", "org.id = :orgId", { orgId })
      .getOne();

I changed the query to inner join and select both related entities. Something like:

const userOrg = await createQueryBuilder('userOrg')
      .innerJoinAndSelect("userOrg.user", "user", "user.username = :username", { username })
      .innerJoinAndSelect("userOrg.org", "org", "org.id = :orgId", { orgId })
      .getOne();

At least for me no need to implement AfterLoad.

Of course I do not have his code to test it ;) My 2c.

FarahKa commented 3 years ago

Hello. I found loading the relations that you'll change/use with the entity worked for me. Something like let something = await this.somethingRepository.findOne({where:{cond1: 1}, relations: ["relation1", "relation2"]}) Hope that helps the next people who get this error.

shahidkhan007 commented 2 years ago

In my case, I was trying to remove an entity whose creation failed and thus didn't have an ID, but other fields were present.