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

Support discriminator multitenancy #2876

Merged
merged 3 commits into from May 17, 2024
Merged

Support discriminator multitenancy #2876

merged 3 commits into from May 17, 2024

Conversation

dstepanov
Copy link
Contributor

No description provided.

*/
@Deprecated(since = "4.8", forRemoval = true)
boolean updateable() default true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use @AliasFor so if someone sets this it sets the new one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, we would need to check for both values to have it backward compatible. I would just use the old value and switch to the new one in M5

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are telling people to use the new method updatable in the javadoc. Both methods should work undateable and updatable`.

would just use the old value and switch to the new one in M5

If only the deprecated one updateable is going to work, then we should not add updatable until the next major version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would just use the old value - I mean we alias the new one to the old one for now

@Documented
@Experimental
@AutoPopulated(updatable = false)
public @interface TenantId {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the target support METHOD and PARAMETER to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would need to add more tests to support update(..., @TenantId String some)

@@ -72,7 +72,7 @@ public class MethodMatchContext extends MatchContext {
@NonNull Map<ClassElement, FindInterceptorDef> findInterceptors) {
super(queryBuilder, repositoryClass, visitorContext, methodElement, typeRoles, returnType, parameters, findInterceptors);
this.entity = entity;
this.parametersInRole = Collections.unmodifiableMap(parametersInRole);
this.parametersInRole = new HashMap<>(parametersInRole);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? Are we now mutating this somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Object value = p.getValue();
if (value != null) {
if (value instanceof String expression) {
// TODO: Support adding an expression annotation value in Core
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you create an issue for this in core and link here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@sdelamo sdelamo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to add docs to this PR.

Copy link
Contributor

@sdelamo sdelamo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we add the annotations @TenantId, @WithTenantId, @WithoutTenantId to the Micronaut Multitenancy module instead of adding them in Micronaut Data.

@dstepanov
Copy link
Contributor Author

I think we add the annotations @TenantId, @WithTenantId, @WithoutTenantId to the Micronaut Multitenancy module instead of adding them in Micronaut Data.

Not sure they will be helpful to outside of Micronaut Data, @TenantId also depends on @AutoPropulated

@dstepanov dstepanov marked this pull request as ready for review April 19, 2024 12:24
@dstepanov
Copy link
Contributor Author

I'm thinking that TenantResolver#resolveTenantIdentifier should probably expect PropagatedContext in the future version.

@dstepanov dstepanov changed the title TenantId Support discriminator multitenancy Apr 19, 2024
@WithTenantId("foo")
List<AccountRecord> findAll$withTenantFoo();

@WithTenantId("#{this.barTenant()}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can another method parameter be used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this moment it will fail trying to resolve parameter as an entity property filter

Copy link
Contributor

@sdelamo sdelamo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no documentation about @WithoutTenantId or @WithTenant

*/
@Deprecated(since = "4.8", forRemoval = true)
boolean updateable() default true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are telling people to use the new method updatable in the javadoc. Both methods should work undateable and updatable`.

would just use the old value and switch to the new one in M5

If only the deprecated one updateable is going to work, then we should not add updatable until the next major version.

Copy link
Contributor

@sdelamo sdelamo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given a domain such as:

package example.micronaut;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.TenantId;
import io.micronaut.serde.annotation.Serdeable;

@Serdeable // <1>
@MappedEntity // <2>
public record Book(@Nullable
                   @Id // <3>
                   @GeneratedValue // <4>
                   Long id,
                   String title,

                   @TenantId // <5>
                   String framework) {
}

and a controller such as:

package example.micronaut;

import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.*;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import java.util.List;

@Controller("/books") // <1>
class BookController {
    private final BookRepository bookRepository;

    BookController(BookRepository bookRepository) { // <2>
        this.bookRepository = bookRepository;
    }

    @ExecuteOn(TaskExecutors.BLOCKING) // <3>
    @Get // <4>
    List<Book> index() {
        return bookRepository.findAll();
    }
}

and a repository such as:

package example.micronaut;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

@JdbcRepository(dialect = Dialect.H2) // <1>
public interface BookRepository extends CrudRepository<Book, Long> {  // <2>
    Long save(String title);
}

I expected this test to pass:

package example.micronaut;

import io.micronaut.context.annotation.Property;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP") // <1>
@Property(name = "datasources.default.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.default.username", value = "sa")
@Property(name = "datasources.default.password", value = "")
@Property(name = "datasources.default.dialect", value = "H2")
@Property(name = "datasources.default.driver-class-name", value = "org.h2.Driver")
@Property(name = "micronaut.multitenancy.tenantresolver.httpheader.enabled", value = StringUtils.TRUE)
@MicronautTest(transactional = false) // <2>
class BookControllerTest {

    @Test
    void multitenancyRequest(@Client("/") HttpClient httpClient, // <3>
                             BookRepository bookRepository) {
        BlockingHttpClient client = httpClient.toBlocking();
        save(bookRepository, client,  "Building Microservices with Micronaut", "micronaut");
        save(bookRepository, client, "Introducing Micronaut", "micronaut");
        save(bookRepository, client, "Grails 3 - Step by Step", "grails");
        save(bookRepository, client, "Falando de Grail", "grails");
        save(bookRepository, client, "Grails Goodness Notebook", "grails");

        List<Book> books = fetchBooks(client, "micronaut");
        assertNotNull(books);
        assertEquals(2, books.size());

        books = fetchBooks(client, "grails");
        assertNotNull(books);
        assertEquals(3, books.size());
        bookRepository.deleteAll();
    }

    List<Book> fetchBooks(BlockingHttpClient client, String framework) {
        HttpRequest<?> request = HttpRequest.GET("/books").header("tenantId", framework);
        Argument<List<Book>> responseArgument = Argument.listOf(Book.class);
        HttpResponse<List<Book>> response = assertDoesNotThrow(() -> client.exchange(request, responseArgument));
        assertEquals(HttpStatus.OK, response.getStatus());
        return response.body();
    }

    void save(BookRepository bookRepository, BlockingHttpClient client, String title, String framework) {
        bookRepository.save(new Book(null, title, framework));
    }
}

However, the usage of repository methods such as deleteAll() or save with an entity containing a populated property with @TenantId does not work.

@dstepanov
Copy link
Contributor Author

@sdelamo Both updatable values should work, but the deprecation now suggests switching to the new one. The old one will be removed in the next major.

@dstepanov
Copy link
Contributor Author

There is no documentation about @WithoutTenantId or @WithTenant

There is in discriminatormode

@dstepanov
Copy link
Contributor Author

@sdelamo Added your test and skipped filling the tenant id if one is preset

Copy link
Contributor

@sdelamo sdelamo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove DISCRIMINATOR mode as it currently does not do anything: #2916

@sdelamo
Copy link
Contributor

sdelamo commented Apr 25, 2024

There is no documentation about @WithoutTenantId or @WithTenant

There is in discriminatormode

@dstepanov I created a PR to show an example in the docs:

#2917

Copy link
Contributor

@sdelamo sdelamo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is a bug #2918 or I am missing something obvious in my test.

Copy link

sonarcloud bot commented May 17, 2024

Quality Gate Failed Quality Gate failed

Failed conditions
5 New Critical Issues (required ≤ 0)
1 New Blocker Issues (required ≤ 0)

See analysis details on SonarCloud

Catch issues before they fail your Quality Gate with our IDE extension SonarLint

@dstepanov dstepanov merged commit a8d0d97 into 4.8.x May 17, 2024
50 of 51 checks passed
@dstepanov dstepanov deleted the tende branch May 17, 2024 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Support Multitenancy via discriminator column
3 participants