ebean-orm / ebean

Ebean ORM
https://ebean.io
Apache License 2.0
1.47k stars 260 forks source link

Immutable entities, bean state and save/update/insert operations #3520

Open mschneider-lise opened 1 week ago

mschneider-lise commented 1 week ago

We are considering switching from Spring Data JPA to Ebean, but I'm missing some documentation regarding the internal state of entity beans and its effect on save/update/insert operations.

Here are my questions:

It would be great to have better documentation regarding entity state and save/update/insert operations.

rPraml commented 4 days ago

Hello, I will try to answer some of your questions.

  • We are using immutable entities (kotlin classes with val fields). So everytime we need to apply changes to an entity, we create a new instance of the object (via builder pattern with an inner builder class). Do I have to do a stateless update now or can I still use save()?

You can only use save, if the internal state is also be cloned. Otherwise, you have to do stateless updates or maybe even Dtos may be better for this use case.

  • Are there drawbacks of using immutable entities when using ebean?

Mostly overhead. Ebean is designed to detect changes and only update these changes on save. So there is an EntityBeanIntercept, the ebean enhancer, BeanCollections with change detection and so on. The overhead can be reduced with setReadonly on the query

  • What effect does the bean state have on the save/update/insert methods?

With the state (EntityBeanIntercept) tracks, which properties are dirty and ebean derives, which insert/update query has to be executed

  • What operations cause the bean state to change? Can we change it manually?

Nearly every change on the bean affects the beanstate. There is DB.beanState(bean) to read/write parts of the beanstate. (recommended API)

You can also directly access the complete beanState, when you cast your bean to EntityBean

  • If the bean state contains only a single updated field, will the update query only contain this field?

Yes (unless you do not have other generated fields, like lastModified)

It would be great to have better documentation regarding entity state and save/update/insert operations.

😉

I would say, your use case should be possible (clone the EBI in your factory and calling save can work), but as mentioned above, ebean is not primary designed for this use case.

There is the persistence context, that tracks beans with same ID and there is also lazy loading, which should not be triggered on cloning. So, you will need a good understanding, what's going on behind the scenes...

Take a look also on readOnly queries and try to implement a PoC on this.

Hope that helps

Roland

rbygrave commented 2 days ago

This reminds me of a conversation I had on reddit recently on this topic. I wonder if you are the same person? Anyway ...

If the bean state contains only a single updated field, will the update query only contain this field?

The DEFAULT is to only update properties that have changed. This is configuration that can change globally (via DatabaseBuilder.setUpdateAllPropertiesInBatch()) and per transaction via transaction.setUpdateAllLoadedProperties().

Interestingly this wasn't always the default, we changed to this a fairly long time ago (maybe 10 years, I can't remember). The argument for this default is that in practice we still get good hit ratio on PreparedStatements because per use case typically the same properties are mutated. So yes a lower hit ratio on PreparedStatements but in practice it's still a good hit ratio and with only updating dirty properties less data is sent over to the database and it's cheaper in terms of network and database - so the argument is that it's a net win to only update the dirty properties.

Devs who disagree can set the global default to include all [loaded] properties in the update. For ebean it's the loaded properties because ebean supports partial objects [like eclipselink, unlike hibernate].

using immutable entities

I've been thinking about this for a while. This might get a lot more interesting when Java gets "withers" but yes effectively the bean state would need to be transferred to a "transformed entity".

readOnly queries

Note that "Unmodifiable graphs" are work-in-progress improvement on readOnly so just note that.

mschneider-lise commented 2 days ago

Thanks for the answers!

I'm struggling a bit trying to copy and modify the entity state.

You can only use save, if the internal state is also be cloned

There is DB.beanState(bean) to read/write parts of the beanstate.

How do I clone the entity state, modify it and apply it to another entity? I can pass the bean state to my builder class but how do I modify it and how do I apply it to the newly created entity?

Can I use the BeanState API to manually change the state? There is only the setPropertyLoaded() method, I can't find any methods to set fields. There is changedProps() but this seems to return a cloned instance of the list.

Here is an (minimal) example of our entity classes:

@Entity
@Table(name = "users")
class UserEntity(
    @Id
    @Column
    val id?: Long,

    @Column
    val name: String
) {
    fun toBuilder() = Builder(
        id = id,
        name = name
    )

    inner class Builder(
        val id: Long,
        var name: String
        // I could pass the bean state or the full object
        // var state: BeanState
    ) {
        fun name(name: String): Builder = apply {
            this.name = name
            // How to manually trigger change detection?
            // What I'd like to do: this.state.setFieldChanged("name")
        }

        fun build(): UserEntity {
            val user = UserEntity(
                id = id,
                name = name
            )

            // How to apply the modified state to the new user instance?
            // DB.beanState(user).apply(state)

            return user
        }
    }
}

This reminds me of a conversation I had on reddit recently on this topic. I wonder if you are the same person? Anyway ...

Not the same person 🙂