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

UnexpectedAccessToTheDatabase error when updating (session.merge()) an existing entity with a ManyToMany relation, which uses a JoinTable with composite id. #2026

Open
BertSuffys opened this issue Dec 9, 2024 · 1 comment

Comments

@BertSuffys
Copy link

Allow me to clarify;

My system has Users and roles. A user can have many roles, and each role can belong to multiple users. A User has a list of roles, mapped as ManyToMany, fetched eagerly. It is also using an intermediary table with a composite id.

Consider the code below;

public void updateUser(RoutingContext routingContext) {
    Map<String, String> postBody = getRequestBody(routingContext);
    if (postBody != null) {
      String id = postBody.get("id");
      if (id != null) {
        userRepository.getUserById(Long.decode(id)).onComplete(cmsUserGet -> {
          if (cmsUserGet.succeeded()) {

            // language
            String urlLang = routingContext.request().getParam("lang");
            Lang lang = urlLang != null ? Lang.createLang(urlLang) : LangUtil.getConfigDefaultLang();
            CmsUser cmsUser = cmsUserGet.result();
            cmsUser.setLang(lang);

            this.roleRepository.getAllCmsRoles().onComplete(roles -> {

              // Retrieve fields
              String firstname = postBody.get("firstname");
              String lastname = postBody.get("lastname");
              String email = postBody.get("email");
              String title = postBody.get("title");
              String password = AuthenticationUtil.bCrypt(postBody.get("password"));
              List<Long> roleIds = Arrays.stream(postBody.get("roles").split(",")).map(Long::parseLong).toList();
              List<CmsRole> selectedRoles = roles.result().stream().filter(obj -> roleIds.contains(obj.getId())).toList();

              // Update fields
              cmsUser.getRoles().clear();
              cmsUser.getRoles().addAll(selectedRoles);
              cmsUser.setFirstname(firstname);
              cmsUser.setLastname(lastname);
              cmsUser.setEmail(email);
              cmsUser.setTitle(title);
              if(password != null){
                cmsUser.setPassword(password);
              }

              // Update
              userRepository.updateUser(cmsUser).onComplete(cmsUserUpdate -> {
                if (cmsUserUpdate.succeeded()) {
                  ResponseUtil.ok(routingContext, new CmsUserDTO(cmsUser, lang).serializeJson().encode());
                } else {
                  ResponseUtil.internal_server_error(routingContext, "Something went wrong updating the CMSRole");
                }
              });
            });
          } else {
            ResponseUtil.internal_server_error(routingContext, "Something went wrong retrieving the CmsRole by the provided ID.");
          }
        });
      } else {
        ResponseUtil.bad_request(routingContext, "No CmsRole id was provided.");
      }
    }
  }

The problem:

In the above code an existing user is updated and persisted. The user is retrieved by Id, all Roles are retrieved, the user is updated given the provided data & chosen roles, and persisted again. The problem occurs in userRepository.updateUser(cmsUser). I've been able to figure out that the issue must be related to the relationship between Users and Roles. If a user gets persisted, and has less roles coupled to it than it did before, i get no issues. As soon as an existing role that was not yet coupled to the user, does get coupled, i get the error on persisting. Below i will provide the specific error as well as some other classes. This is my first issue post, so hopefully it is formatted in a somewhat proper manner. Thanks alot in advance because this one is really puzzling me :o

org.hibernate.HibernateException: java.util.concurrent.CompletionException: org.hibernate.reactive.event.impl.UnexpectedAccessToTheDatabase: Unexpected access to the database

Repository methods:

  public Future<CmsUser> getUserById(Long id) {
    CompletionStage<CmsUser> cmsRoleCompletionStage = super.getSessionFactory().withTransaction((session, transaction) ->
      session.find(CmsUser.class, id)
    );
    return Future.fromCompletionStage(cmsRoleCompletionStage).map(role -> role); // Maps directly to null if not found
  }
  
 public Future<CmsUser> updateUser(CmsUser updatedCmsUser) {
    CompletionStage<CmsUser> updateStage = super.getSessionFactory().withTransaction((session, transaction) -> {
      return session.merge(updatedCmsUser);
    });
    return Future.fromCompletionStage(updateStage);
  }
  
  
public Future<List<CmsRole>> getAllCmsRoles() {
    CompletionStage<List<CmsRole>> cmsRolesCompletionStage = super.getSessionFactory().withTransaction((session, transaction) ->
      session.createQuery("FROM CmsRole", CmsRole.class).getResultList()
    );
    return Future.fromCompletionStage(cmsRolesCompletionStage).map(roles -> roles);
 }

CmsUser:

@Entity
@Table(name="cms_user")
public class CmsUser extends Translated {

  /* FIELDS */
  private Long id;
  private String email;
  private String password;
  private List<CmsRole> roles = new ArrayList<>();
  private String firstname;
  private String lastname;

  /* CONSTRUCTOR */
  public CmsUser(){
      super();
  }

  @Override
  public Translation getUntranslated(Lang lang) {
    CmsUserLang cmsUserLang = new CmsUserLang();
    cmsUserLang.setId(null); // signals non-db entity
    cmsUserLang.setTitle(ConfigHolder.project_config.getJsonObject("framework").getString("missing_translation_text"));
    cmsUserLang.setLang(lang);
    return cmsUserLang;
  }

  public CmsUser(String email, String password){
    this();
    setEmail(email);
    setPassword(password);
  }

  public static CmsUser init(String firstname, String lastname, String email, String password, String title, List<CmsRole> roles, Lang lang){
    CmsUser cmsUser = new CmsUser();
    cmsUser.setFirstname(firstname);
    cmsUser.setLastname(lastname);
    cmsUser.setEmail(email);
    cmsUser.setRoles(roles);
    cmsUser.setPassword(AuthenticationUtil.bCrypt(password));
    // lang
    CmsUserLang cmsUserLang = new CmsUserLang();
    cmsUserLang.setTitle(title);
    cmsUserLang.setTranslated(cmsUser);
    cmsUserLang.setLang(lang);
    cmsUser.addTranslation(cmsUserLang);
    return cmsUser;
  }

  @Transient
  public void setLang(Lang lang) {
    try{
      super.setLang(lang, CmsUserLang.class, this);
    }catch (TranslationNotFoundException e){
      e.printStackTrace();
    }
  }

  /* GETTERS AND SETTERS */
  @OneToMany(mappedBy = "translated", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, targetEntity = CmsUserLang.class)
  public List<Translation> getTranslations() {
    return super.getTranslations();
  }
  public void setTranslations(List<Translation> translations) {
    super.setTranslations(translations);
  }

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "cms_user_seq")
  @SequenceGenerator(name = "cms_user_seq", sequenceName = "cms_user_seq")
  public Long getId() {
    return id;
  }

  @ManyToMany(fetch = FetchType.EAGER)
  @JoinTable(
    name = "cms_user_role",
    joinColumns = @JoinColumn(name = "user_id"),
    inverseJoinColumns = @JoinColumn(name = "role_id")
  )
  public List<CmsRole> getRoles() {
    return roles;
  }

  @Transient
  public List<Long> getRoleIds() {
    List<Long> roleIds = new ArrayList<>();
    if (roles != null) {
      for (CmsRole role : roles) {
        roleIds.add(role.getId());
      }
    }
    return roleIds;
  }

  @Override
  public String toString(){
    return String.format("%s %s %s", this.firstname, this.lastname, this.email);
  }
  @Column(length = 500)
  public String getFirstname() {
    return firstname;
  }
  public void setFirstname(String firstname) {
    this.firstname = firstname;
  }
  @Column(length = 500)
  public String getLastname() {
    return lastname;
  }
  public void setLastname(String lastname) {
    this.lastname = lastname;
  }
  @Transient
  public String getTitle() {
    return ((CmsUserLang)super.getActiveTranslation()).getTitle();
  }
  public void setTitle(String title) {
     ((CmsUserLang)super.getActiveTranslation()).setTitle(title);
  }

  public void setId(Long id) {
    this.id = id;
  }

  public void setRoles( List<CmsRole> roles) {
    this.roles = roles;
  }
  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

}

A CmsRole

@Entity
@Table(name="cms_role")
public class CmsRole extends Translated {

  /* FIELDS */
  private Long id;
  private String code;
  private int score;

  /* METHODS */
  @Transient
  public void setLang(Lang lang) {
    try{
      super.setLang(lang, CmsRoleLang.class, this);
    }catch (TranslationNotFoundException e){
      e.printStackTrace();
    }
  }

  /* CONSTRUCTOR */
  public CmsRole(){
  }

  @Override
  public CmsRoleLang getUntranslated(Lang lang) {
    CmsRoleLang cmsRoleLang = new CmsRoleLang();
    cmsRoleLang.setId(-1L); // signals non-db entity
    cmsRoleLang.setName(ConfigHolder.project_config.getJsonObject("framework").getString("missing_translation_text"));
    cmsRoleLang.setLang(lang);
    return cmsRoleLang;
  }

  public static CmsRole init(String code, String name, int score, Lang lang){
    CmsRole cmsRole = new CmsRole();
    cmsRole.setCode(code);
    cmsRole.setScore(score);
    CmsRoleLang cmsRoleLang = new CmsRoleLang();
    cmsRoleLang.setName(name);
    cmsRoleLang.setTranslated(cmsRole);
    cmsRoleLang.setLang(lang);
    cmsRole.addTranslation(cmsRoleLang);
    return cmsRole;
  }

  /* GETTER AND SETTER */
  @OneToMany(mappedBy = "translated", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, targetEntity = CmsRoleLang.class)
  public List<Translation> getTranslations() {
    return super.getTranslations();
  }
  public void setTranslations(List<Translation> translations) {
    super.setTranslations(translations);
  }

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "cms_role_seq")
  @SequenceGenerator(name = "cms_role_seq", sequenceName = "cms_role_seq")
  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  @Override
  @Transient
  public String toString() {
    // Ex: {id:5, name:'client'}
    return String.format("{\"id\":%d, \"name\":\"%s\"}", this.id, this.getName());
  }
  @Transient
  public String getName() {
    return ((CmsRoleLang)super.getActiveTranslation()).getName();
  }
  public void setName(String name) {
    ((CmsRoleLang)super.getActiveTranslation()).setName(name);
  }

  @Column(length = 100)
  public String getCode() {
    return code;
  }

  public void setCode(String code) {
    this.code = code;
  }
  public int getScore() {
    return score;
  }
  public void setScore(int score) {
    this.score = score;
  }
}


@Vincentelias
Copy link

Have you tried turning it on and off again?

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

2 participants