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

Allow hydration of virtual properties in embeddables #5578

Closed
toonvanstrijp opened this issue May 15, 2024 · 14 comments · Fixed by #5579
Closed

Allow hydration of virtual properties in embeddables #5578

toonvanstrijp opened this issue May 15, 2024 · 14 comments · Fixed by #5579
Labels
enhancement New feature or request

Comments

@toonvanstrijp
Copy link
Contributor

toonvanstrijp commented May 15, 2024

Is your feature request related to a problem? Please describe.
Assume we're having the follow code. As you can see all the properties on the embeddable are marked as persistent: false. This because they are not actual database column. The embeddable documentation states it help you separate concern by allowing the embeddable to be defined on multiple entities. In our use case we have multiple entities making use of this embeddable.

Right now as part of the metadata discovery process all persistent: false are ignored on embeddable properties. This is causes the hydration to fail since it doesn't know about those properties.

@Entity()
export class EventEntity {
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
  id: string;

  @Property()
  name: string;

  @Embedded(() => Statistic, {
    nullable: true,
    prefix: false,
    entity: () => Statistic,
  })
  statistic?: Statistic;
}


@Embeddable()
export class Statistic {
  @Property({ type: 'int', persist: false })
  sales?: number;

  @Property({ type: 'int', persist: false })
  tickets?: number;

  @Property({ type: DoubleType, persist: false })
  revenue?: number;

  @Property({ type: DoubleType, persist: false })
  cost?: number;

  @Property({ type: DoubleType, persist: false })
  profit?: number;

  @Property({ type: 'int', persist: false })
  collected?: number;
}

Describe the solution you'd like
I would expect this query to be mapped correctly to the classes described above

const qb = em.createQueryBuilder(EventEntity, 'e');

    return qb
      .select([
        'e.*',
        'sum(oi.amount) as tickets',
        'sum(oi.amount * oi.price) as revenue',
        'sum(oi.amount * p.purchase_price) as costs',
        'count(distinct oi.order_id) as sales',
        'sum(oi.collected) as collected',
        'max(o.sold_at) as last_sold_at',
      ])
      .leftJoin('e.orderItems', 'oi', { 'oi.event_id': em.raw('e.id') })
      .leftJoin('oi.order', 'o', { 'oi.order_id': em.raw('o.id') })
      .leftJoin('oi.product', 'p', { 'oi.product_id': em.raw('p.id') })
      .groupBy('e.id');

Describe alternatives you've considered
We tried to look at the virtual entities but this requires us to make a whole new entity and doesn't give use the freedom around sorting and mapping the results right now.

@B4nan
Copy link
Member

B4nan commented May 15, 2024

This is causes the hydration to fail since it doesn't know about those properties.

You said the properties don't map to actual db columns, right? So what hydration, how is it triggered (e.g. em.find or em.create? both are using hydrator internally) and where are the data coming from?

persist: false means it's not a real db column, so there is nothing to hydrate from.

@toonvanstrijp
Copy link
Contributor Author

toonvanstrijp commented May 15, 2024

@B4nan I've updated the description with the createQueryBuilder and query that I'm using. Hopefully this helps you understand the problem. Maybe we're using MikroORM wrong, but for us this seems like the proper way of adding a statistic embeddable to our entities.

@B4nan
Copy link
Member

B4nan commented May 15, 2024

That's surely not what embeddables were meant for, they represent actual values stored in the database.

@toonvanstrijp
Copy link
Contributor Author

toonvanstrijp commented May 15, 2024

@B4nan What would you suggest doing? I'm trying to find a solution in the documentation but I can't find a good solution. Is there already something we could use to map the query result? Maybe doing it this way via the @Embeddable is a good solution?

Also here you're mentioning a solution by using virtual properties by doing persistant: false. This is what I'm trying to do as well. Only thing is, virtual properties in embeddables don't get serialized.

@toonvanstrijp toonvanstrijp changed the title Allow hydration of non persistent embeddable properties Allow hydration of virtual properties in embeddables May 15, 2024
@B4nan
Copy link
Member

B4nan commented May 15, 2024

I am not completely opposed to the idea, especially since it's not breaking anything apparently.

It feels a bit weird to achieve this by adding persist: false to every single scalar inside the embedded properties, it would be much cleaner to have that only on the @Embedded one itself (but I guess that would technically only cascade this to all the properties in the embeddable).

@toonvanstrijp
Copy link
Contributor Author

@B4nan should the @Embedded(() => Statistic, { persist: false }) override any @Property({ persist: true }) in the embeddable, or would it be better to add a persist: true on the @Embeddable decorator. That way it's less confusion maybe.

@B4nan
Copy link
Member

B4nan commented May 15, 2024

I don't think it should override anything, by declaring it on the parent you make everything inside virtual. persist: true is the default.

@toonvanstrijp
Copy link
Contributor Author

toonvanstrijp commented May 15, 2024

@B4nan but what to do in this case? Should it persist views or not? Right now it doesn't override anything. So the views column gets persisted and saved in the database.

@Entity()
class User {

  @PrimaryKey()
  id!: number;

  @Embedded(() => Statistic, { prefix: false, nullable: true, persist: false })
  statistic?: Statistic;
}

@Embeddable()
class Statistic {

    @Property({ type: DoubleType })
    total: number;

    @Property({ type: DoubleType, persist: true })
    views!: number;

    constructor(total: number) {
      this.total = total;
    }

}

@B4nan
Copy link
Member

B4nan commented May 15, 2024

No, I don't think so. This couldn't work in object mode at all anyway.

@toonvanstrijp
Copy link
Contributor Author

I'm not using object mode right? And if it should not persist the view column it should override persist: true in this case right? Just making sure everything works correctly :)

@B4nan
Copy link
Member

B4nan commented May 15, 2024

It should work in both modes, and yes, I would override it, since persist: true is technically the default anyway.

@toonvanstrijp
Copy link
Contributor Author

Maybe I need to add a test case for object mode as well then?

@B4nan
Copy link
Member

B4nan commented May 15, 2024

Let's focus on the default inline mode now.

@toonvanstrijp
Copy link
Contributor Author

Do no more changes are required?

B4nan added a commit that referenced this issue May 20, 2024
Closes #5578

---------

Co-authored-by: Martin Adámek <banan23@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants