Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concrete type isn't written down in table inheritance #2544

Closed
lev-kuznetsov opened this issue Jul 18, 2018 · 10 comments · Fixed by #6219
Closed

Concrete type isn't written down in table inheritance #2544

lev-kuznetsov opened this issue Jul 18, 2018 · 10 comments · Fixed by #6219

Comments

@lev-kuznetsov
Copy link

Issue type:

[X] question
[X] bug report

Database system/driver:

[X] postgres
[X] sqlite

TypeORM version:

[X] 0.2.7 (or put your version here)

Steps to reproduce or a small repository showing the problem:

Maybe I'm doing something wrong; when I create a polymorphic container and save, writing down the entities the type written down for each entity is the supertype not the concrete type. Here's a snippet to reproduce, I also tried postgres with the same result

import {
  ChildEntity, Column, createConnection, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance
} from 'typeorm'

@Entity()
export class ContainerOfThings {
  constructor(things?: Thing[]) {
    this.things = things
  }

  @PrimaryGeneratedColumn()
  private id: number

  @OneToMany(() => Thing, thing => thing.container, {cascade: ['insert', 'remove']})
  things: Thing[]
}

@Entity()
@TableInheritance({column: {type: 'varchar', name: 'type'}})
export abstract class Thing {
  @PrimaryGeneratedColumn()
  private id: number

  @ManyToOne(() => ContainerOfThings, c => c.things)
  container: ContainerOfThings
}

@ChildEntity()
export class ParticularThing extends Thing {
  constructor(foo: string = 'foo') {
    super()
    this.foo = foo
  }

  @Column({type: 'varchar'})
  foo: string
}

describe('class inheritance', () => {
  it('should write down type', async () => {
    const connection = await createConnection({
      synchronize: true,
      type: 'sqlite',
      database: 'inheritance-test',
      logging: true,
      entities: [ContainerOfThings, Thing, ParticularThing]
    })

    await connection.transaction(async em => {
      await em.getRepository(ContainerOfThings).save(new ContainerOfThings([
        new ParticularThing('bar'), new ParticularThing('baz'), new ParticularThing()
      ]))
    })
  })
})

If you look at the logs and also inspect the database you'll see it's written down Thing as type not ParticularThing

@lev-kuznetsov
Copy link
Author

I saw #184

I tried saving using the concrete repository and the correct type is written down. After that I can use the base repository to findX and that gets resolved to the correct type which I would think would be the most difficult thing. So is writing down the base type when using the base repository a bug?

@RobinFalko
Copy link

I encounter the same issue. Even storing a base entity using a child repo will set the child type to the base type, that's very unhandy when someone needs to store a set of data with mixed types.

@Krivich
Copy link

Krivich commented Jul 23, 2019

Hi! I have the same problem. I'm using cascade write to write Provider and its Resources in single request:
@OneToMany(type => ResourceEntity, resources => resources.provider, {cascade: true, eager: true}) resources: ResourceEntity[];

Therea are multiple different Resource types available:
@Entity() @TableInheritance({ column: { name: 'discriminator', type: 'varchar' } }) export abstract class ResourceEntity extends TariffableEntity{

@ChildEntity("BicycleEntity") export class BicycleEntity extends ResourceEntity{

After persistance I see that every row at database is marked with ResourceEntity in the discriminator column. Resource-childs-specific fields are persisted well. But when I read Provider from the database, I receive ResourceEntity collection where ResourceEntity-only fields populated.

I've tried to add explicit column discriminator:string = "BicycleEntity" to the clild entity, but it didn't help, this value being replaced with ResourceEntity on write.

Is there any working workaround avaliable?

@daweedm
Copy link

daweedm commented Jul 30, 2019

Same

@ulo
Copy link

ulo commented Feb 21, 2020

Hi,
I had the same problem and I do not think to save each related entity explicitly is a solution (what's the point on cascade then?). I had a closer look at the code and I found two classes which need some change for this to work:

  1. In query-builder/transformer/PlainObjectToNewEntityTransformer.ts (lines 64, 69, 73) I replaced relation.inverseEntityMetadata with relation.inverseEntityMetadata.childEntityMetadatas.find(metadata => metadata.name == objectRelatedValueItem.constructor.name)

  2. In metadata/EntityMetadata.ts (lines 698, 700) I replaced relation.inverseEntityMetadata with relation.inverseEntityMetadata.childEntityMetadatas.find(metadata => metadata.name == value.constructor.name)

With this, entities will be created not based on the type of the relation, which is the super-class, but based on the sub-class as defined by the given object itself. And thus the discriminating column in the single-table-model is set correctly.

My little hack for sure needs some more testing and work (so that it will also work for relations without inheritance), but I hope that one of the developers might pick this up and fix this bug!

And many thanks for this great ORM!

@EgorGumin
Copy link
Contributor

EgorGumin commented Apr 7, 2020

Any progress here?
This bug makes single table inheritance with cascade unusable

@kshitij-srv
Copy link

Same here. I have been forced twice to find workarounds due to this problem. I see the issue dates back to 2018 and still hasn't been addressed.

@ZBAGI
Copy link
Contributor

ZBAGI commented Jun 3, 2020

Its been 2 years and bug still seems to be there.
For those who do not want to modify typeorm source code, but still want this to work, here is bugfix
( based on @ulo solution )

Create file:

import { EntityMetadata, ObjectLiteral } from "typeorm";
import { RelationMetadata } from "typeorm/metadata/RelationMetadata";

/*
Fix for wrong metadata being used when using table inheritance with cascade actions.
Read more: https://github.com/typeorm/typeorm/issues/2544
*/

function getInverseEntityMetadata(value: any, relation: RelationMetadata): EntityMetadata {
	return relation.inverseEntityMetadata.childEntityMetadatas.find(metadata => {
		return metadata.name == value.constructor.name;
	}) 
	// if no childEntityMetadata found return value that is used originaly by typeorm
	?? relation.inverseEntityMetadata;
}

EntityMetadata.prototype.extractRelationValuesFromEntity = function(entity: ObjectLiteral, relations: RelationMetadata[]): [RelationMetadata, any, EntityMetadata][] {
	// Exact copy of extractRelationValuesFromEntity method, except we calling getInverseEntityMetadata
	// instead of using `relation.inverseEntityMetadata` which is incorrect for childEntity
	const relationsAndValues: [RelationMetadata, any, EntityMetadata][] = [];
	relations.forEach(relation => {
		const value = relation.getEntityValue(entity);
		if (Array.isArray(value)) 
			value.forEach(subValue => relationsAndValues.push([relation, subValue, /* BUGFIX*/getInverseEntityMetadata(subValue, relation)]));
		else if (value) 
			relationsAndValues.push([relation, value,  /* BUGFIX*/getInverseEntityMetadata(value, relation)]);
	});
	
	return relationsAndValues;
};

import or require file somewhere in your code (preferably once) i do it right before createConnection. So far it works fine.

@EgorGumin
Copy link
Contributor

@ZBAGI will you make a pull request with these changes?

@kecoliva
Copy link

kecoliva commented Oct 22, 2022

Database system/driver:
[X] postgres

TypeORM version:
[X] 0.3.10

Am I the only one or is this bug still present?

@Entity('survey')
export class SurveyEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  title: string;

  @Column()
  description: string;

  @OneToMany(() => QuestionEntity, ({ survey }) => survey, {
    cascade: true,
  })
  questions: Array<QuestionEntity>;
}
@Entity('question')
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export abstract class QuestionEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ enum: QuestionType })
  type: QuestionType;

  @Column()
  question: string;

  @ManyToOne(() => SurveyEntity, ({ questions }) => questions, {
    onDelete: 'CASCADE',
  })
  survey: SurveyEntity;
}
@ChildEntity(QuestionType.CHOICE)
export class ChoiceQuestionEntity extends QuestionEntity {
  @Column({
    array: true,
  })
  choices: Array<string>;
}
async create({ title, description, questions }: CreateSurveyDto) {
  return await this.surveysRepository.save({
    title,
    description,
    questions,
  });
}

The child's type value is QuestionEntity and the choices array is not being saved in the table.
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants