I'm new to hibernate-reactive, smallrye mutini, PanacheEntity, PanacheRepository.
At a high level I have:
Tenant - a domain object (I don't want database related code in this)
TenantDAO - A dao object that is specific to the db
SqlRepository - A base class for repository that stores the dao and maps the dao <-> entity
TenantRepository - extends SqlRepository and provides the TenantDAO object
A rest handler for POST that calls the repository to store the tenant info.
Here's my code:
Tenant.java (Domain entity)
package com.my.platform.model.user;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class Tenant {
private String id; // an application specific id (not the PanacheEntity id)
private String name;
}
TenantDAO.java (object stored in the db):
package com.my.platform.repository.user;
import com.my.platform.model.user.Tenant;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "Tenants")
public class TenantDAO extends PanacheEntity {
private String entityId;
private String name;
public TenantDAO(Tenant tenant) {
this.entityId = tenant.getId();
this.shortName = tenant.getShortName();
}
public Tenant getEntity() {
return new Tenant(entityId, name);
}
}
I also have this file in the same older:
package-info.java
@io.quarkus.hibernate.orm.PersistenceUnit("<default>")
package com.my.platform.repository.user;
TenantRepository.java:
package com.my.platform.repository.user;
import com.my.platform.model.user.Tenant;
import com.mys.platform.repository.impl.sql.SqlRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.ext.Provider;
@ApplicationScoped
public class TenantRepository extends SqlRepository<Tenant, TenantDAO> {
@Override
public TenantDAO createDAO(Tenant tenant) {
return new TenantDAO(tenant);
}
}
SqlRepository:
package com.my.platform.repository.impl.sql;
import java.util.List;
import io.quarkus.hibernate.reactive.panache.PanacheRepository;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
@ApplicationScoped
public abstract class SqlRepository<E, DAO> implements PanacheRepository<DAO> {
@Override
@WithSession
public Uni<List<E>> list() {
return listAll().onItem().transform(entityList ->
entityList.stream().map(entityDao -> entityDao.getEntity()).toList()
);
}
@Override
@WithSession
public Uni<E> findByEntityId(String id) {
return find("entityId", id).firstResult().map(dao -> (E)dao.getEntity());
}
@Override
@Transactional
public Uni<E> add(E entity) {
DAO dao = createDAO(entity);
persist(dao);
return Uni.createFrom().item(dao.getEntity());
}
abstract protected DAO createDAO(E);
}
REST resource method (TenantCreationData is a simple POJO for getting tenant info):
@POST
@Transactional
public Uni<Response> provision(TenantCreationData tenantCreationData) {
Tenant tenant = new Tenant(tenantCreationData.getTenantName());
return tenantRepository.add(tenant)
.onItem()
.transform(addedTenant -> (addedTenant != null) ? Response.ok(addedTenant) : Response.status(Response.Status.NOT_FOUND))
.onItem()
.transform(Response.ResponseBuilder::build);
}
Note:
I don't fully understand the @WithSession as I couldn't find good documentation around that which explains exactly what this session is. I got the code to work by adding this annotation to the get() methods (otherwise it errors out with some session relayed error). And if I add the same @WithSession annotation to the add() method then I get an obscure error that I couldn't find on google search -
Error id 1aff6b15-313d-47a9-9422-97afdca21e95-1, java.lang.IllegalStateException: HR000068: This method should exclusively be invoked from a Vert.x EventLoop thread; currently running on thread 'executor-thread-1'
Anyway, the code as I have shown above doesn't give me any errors but when an HTTP post to it goes through all the code including persist() but it's not saved to the DB.
I am not sure if this is something to do with the session -- persist() doesn't throw any exceptions but data is not saved. Same with persistAndFlush(). I debugged into persist() and deep down inside the persist() method of AbstractJpaOperations, my breakpoint at if(!session.contains(entity)) doesn't hit (see below). Failing somewhere around session handling or "session chaining"? I'm not sure I fully understand the sessions here.
public Uni<Void> persist(Uni<Mutiny.Session> sessionUni, Object entity) {
return sessionUni.chain(session -> {
if (!session.contains(entity)) {
return session.persist(entity);
}
return Uni.createFrom().nullItem();
});
}
application.properties:
# DataSource configuration
quarkus.datasource.url=<mysql db>
quarkus.datasource.driver=com.mysql.cj.jdbc.Driver
quarkus.datasource.username=<username>
quarkus.datasource.password=<password>
# Default persistence unit
quarkus.hibernate-orm.database.generation=none
quarkus.hibernate-orm.datasource=<default>
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.multitenant=SCHEMA
# Tenant persistence unit
quarkus.hibernate-orm."tenant".database.generation=none
quarkus.hibernate-orm."tenant".datasource=<default>
quarkus.hibernate-orm."tenant".log.sql=true
quarkus.hibernate-orm."tenant".multitenant=SCHEMA
# Hibernate ORM configuration
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.database.default-schema=shared
Multi-tenancy config classes:
@PersistenceUnitExtension
@RequestScoped
public class DefaultTenantResolver implements TenantResolver {
private static final String DEFAULT_TENANT = "shared";
@Override
public String getDefaultTenantId() {
return DEFAULT_TENANT;
}
@Override
public String resolveTenantId() {
return DEFAULT_TENANT;
}
}
@PersistenceUnitExtension("tenant")
@RequestScoped
public class CustomTenantResolver implements TenantResolver {
private static final String DEFAULT_TENANT = "shared";
@Inject
RoutingContext context;
@Inject
UserService userService;
@Override
public String getDefaultTenantId() {
return DEFAULT_TENANT;
}
@Override
public String resolveTenantId() {
String tenantName = context.request().getHeader("tenant");
return tenantName;
}
}
Any thoughts on what I am missing?
When I POST to my resource, I expect the data to be saved. But it's not saved.
RESOLVED!
Changed this: @Override @Transactional public Uni add(E entity) { DAO dao = createDAO(entity); persist(dao); return Uni.createFrom().item(dao.getEntity()); }
to: