Skip to content

Entity with assigned Id is not inserted #49

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

Closed
rj-hwang opened this issue Jan 11, 2019 · 11 comments
Closed

Entity with assigned Id is not inserted #49

rj-hwang opened this issue Jan 11, 2019 · 11 comments

Comments

@rj-hwang
Copy link

rj-hwang commented Jan 11, 2019

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class People {
  @Id private String id;
  private String name;
}


import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface PeopleRepository extends ReactiveCrudRepository<People, String> {
}


@Autowired private PeopleRepository repository;
@Test
void test() {
  // save it
  People po = new People();
  po.setId(UUID.randomUUID().toString());
  po.setName("simter");
  StepVerifier.create(repository.save(po))
    .expectNext(po)
    .verifyComplete();

  // verify saved
  StepVerifier.create(repository.findById(po.getId()))
    .expectNext(po)
    .verifyComplete();
}

I log out io.r2dbc.h2.client.SessionClient as debug, request a update sql not insert into sql. Entity not saved and repository.findById found nothing.

···
2019-01-11 11:37:00.404 DEBUG io.r2dbc.h2.client.SessionClient : Request: UPDATE people SET name = $2 WHERE id = $1 {1: '1d7d50d3-b1a3-4c27-8552-00d2bf9c5b08', 2: 'simter'}
···

@mp911de
Copy link
Member

mp911de commented Jan 11, 2019

Spring Data isn't using R2DBC Client but is itself a client library. Therefore, this ticket reports a constellation that isn't implemented at all. Spring Data R2DBC logs SQL statements to org.springframework.data.r2dbc.function.

Can you provide a complete, minimal, verifiable sample that reproduces the problem rather than pasted code? It should be available as a GitHub (or similar) project or attached to this issue as a zip file.

rj-hwang added a commit to start-java/start-r2dbc that referenced this issue Jan 13, 2019
rj-hwang added a commit to start-java/start-r2dbc that referenced this issue Jan 13, 2019
@rj-hwang
Copy link
Author

Here is the project contain all above unit test code : https://github.com/start-java/start-r2dbc/tree/spring-data-r2dbc/issue49.

For this issue, code is in branch spring-data-r2dbc/issue49.

@mp911de mp911de changed the title Repository can not save a custom id entity Entity with assigned Id is not inserted Jan 14, 2019
@mp911de
Copy link
Member

mp911de commented Jan 14, 2019

Thanks a lot. This expected behavior. Spring Data inspects the Id field. If the id is null then Spring Data repositories consider the entity new. In this case, the Id is assigned and so Spring Data considers the entity to already exist.

If you want to insert entities with an assiged Id, then either use DatabaseClient.insert() or your entities should implement Persistable to determine whether the entity is new:

public class People implements Persistable<String> {
  @Id
  private String id;
  private String name;

	@Override
	public boolean isNew() {
		return true; // should be backed by code that is able to figure out whether the entity is new.
	}
}

@mp911de mp911de closed this as completed Jan 14, 2019
@Numbernick
Copy link

Thank you for the answer. I have a question to add.
I get a Mono returned from a save. I would expect an empty Mono if the update affected 0 rows (Which will be the case when I update a row with a non existing id). Returning a "saved" entity with a false positive "isNew" is irritating as it does not represent the state of the database.

Also the documentation of the Repository.save(...) states "[...] Use the returned instance for further operations as the save operation might have changed the entity instance completely.". So if no return was given from the database, the entity got changed completly, as it is not present in the database (So changed to "empty")

@hfhbd
Copy link

hfhbd commented Feb 14, 2020

Hey, I am using Kotlin and the data classes as entities.
To have a non null type id, I am using a negative id and conforming to the the Persistable interface.

There is no builtin default handling, when the id is -1 or 0 as indicator to create a row?

Goal:

@Table
data class Test(@Id val id: Int = 0, val name: String)

Workaround:

@Table
data class Test(@Id val id: Int = 0, val name: String): Persistable<Int> {
    override fun isNew() = id == 0
    override fun getId() = id
}

@rezaep
Copy link

rezaep commented Apr 6, 2020

@hfhbd I have the same issue with Kotlin's data classes.
There is no way to assign null as the default value to the id field and extend the Persistable interface at the same time.

@jefersonm
Copy link

If you set id to null, it works without the need of the Persistable interface and the id is autogenerated in the database.

Example:
@Table data class Test(@Id val id: Int? = null, val name: String)

@rezaep
Copy link

rezaep commented Jul 23, 2020

If you set id to null, it works without the need of the Persistable interface and the id is autogenerated in the database.

Example:
@Table data class Test(@Id val id: Int? = null, val name: String)

At the time I've written my comment, it wasn't possible. I'm sure there were so many versions released in this period. Maybe one of them solved this issue.

@jnfeinstein
Copy link

jnfeinstein commented Mar 18, 2021

I just fought with this issue for a few hours. There are many use cases where it is important to insert an existing ID. CQRS is a big one, which should be a perfect use case for a reactive framework.

I eventually landed on this:

@Table
data class AccountView(
    val id: UUID,
    val name: String,
) : BaseView<UUID>(id)

abstract class BaseView<ID>(
    private val id: ID,
) : Persistable<ID> {
    @Transient
    private var isNew = false

    @Id
    override fun getId() = id

    @JsonIgnore // isNew() is picked up by Jackson, am I crazy here?
    override fun isNew() = isNew

    fun markNew() {
        isNew = true
    }
}

accountRepo.save(Account(...).also { it.markNew() })

This seems pretty ugly compared to:

@Table
data class AccountView(
    @Id val id: UUID,
    val name: String,
)

accountRepo.insert(Account(...))

Would it be possible to expose accountViewRepo.insert(...) and accountViewRepo.update(...)? Presumably accountViewRepo.save(...) is making the decision somewhere which to use. I'd like to be able to make that decision rather than having to fight the framework.

Another issue is that the framework does not recognize default arguments, so this does not work:

@Table
data class AccountView(
    @Id private val id: UUID,
    val name: String,
    @Transient private val isNew: Boolean = false
) : Persistable<UUID> {
  override fun getId() = id 

  @JsonIgnore
  override fun isNew() = isNew
}

I think this looks pretty clean, if default arguments were supported (which is presumably a good idea anyways). Still, having the direct methods would be the cleanest since Persistable wouldn't be required at all.

@hantsy
Copy link

hantsy commented Sep 20, 2022

If adding a @Version field(new record is null or 0), it can resolve this issue?

@yktv4
Copy link

yktv4 commented Feb 19, 2024

Experimented a bit on @jnfeinstein's solution to add an .insert extension to the repositories. Let me know what you think.

// BaseEntity.kt

abstract class BaseEntity<ID>(
    private val id: ID,
) : Persistable<ID> {
    @Transient
    private var isNew = false

    @Id
    override fun getId() = id

    @JsonIgnore
    override fun isNew() = isNew

    fun markAsNew() {
        isNew = true
    }
}

fun <T: BaseEntity<ID>, ID> ReactiveCrudRepository<T, ID>.insert(entity: T): Mono<T> =
    save(entity.apply { markAsNew() })
// User.kt

@Table("users")
data class User(
    @Id val id: UUID,

    val email: String,

    @Column("first_name")
    val firstName: String,

    @Column("last_name")
    val lastName: String,
) : BaseEntity<UUID>(id)
// Usage

import your.package.name.entities.insert

userRepository.insert(User(
    id = it.id,
    email = it.email,
    firstName = it.firstName,
    lastName = it.lastName,
))

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

No branches or pull requests

9 participants