calculated fields in Entity: Autowired is null - spring

I have an Entity
#Entity
#Table(name = "orgtree")
public class OrganizationTree {
#Id
#Column(name="ORGANIZATION_ID")
private String organizationId;
#Column(name="ORGANIZATION_NAME")
private String organizationName;
}
and a repository to provide REST access
#RepositoryRestResource(collectionResourceRel = "organizationTree", path = "organizationTree")
public interface OrganizationTreeRepository extends JpaRepository<OrganizationTree,String> {
#Query
#RestResource(path = "findAll", rel = "findAll")
List<OrganizationTree> findAll();
}
So far so good.
Now I want to add a calculated field to my entity
#Autowired
#Transient
private OrgTreeService orgTreeService;
#JsonSerialize
public Integer getPersonCount() {
return orgTreeService.getPersonCount(organizationId);
}
Here I have several problems:
orgTreeService is null
people say that it's a bad practice to use a
service in an Entity
What is the canonical solution to this problem?

A solution I found (or should I call it a hack) is the following:
I annotate a calculated field with a custom serializer:
#Formula(value = "ORGANIZATION_ID")
#JsonSerialize(using=JsonOrgPersonCountSerializer.class)
private String personCount;
In the serializer I compute the person's count:
public class JsonOrgPersonCountSerializer extends JsonSerializer<String> {
#Override
public void serialize(String source, JsonGenerator gen, SerializerProvider prov) throws IOException, JsonProcessingException {
gen.writeString("" + orgTreeService.getPersonCount(source));
}
}
Another solution would be to use some kind of Data Transfer Object where I can call my service.

Related

How do I implement tenant-based routing for elasticsearch in JHipster?

I´m currently trying to implement multi-tenancy into my JHipster microservices. However, I can't find a way to implement tenant-based routing for elasticsearch.
So far I have managed to implement datasource routing for the PostgreSQL DBs similar to the following article: https://websparrow.org/spring/spring-boot-dynamic-datasource-routing-using-abstractroutingdatasource
When I started looking for ways to implement multi tenancy in elasticsearch, I found the following article: https://techblog.bozho.net/elasticsearch-multitenancy-with-routing/
There I read about tenant-based routing. First I tried looking it up on the internet, but anything I found was either over 5 years old or not related to java, much less to Spring/Jhipster. Then I tried looking into the methods of ElasticSearchTemplate, the annotation variables of #Document and #Settings and the configuration options in the .yml file, but didn't find anything useful.
I'm currently using Jhipster version 7.9.3, which uses the Spring-Boot version 2.7.3. All the microservices were created with JDL and on half of them I put elasticsearch into the configuration. The other half does not matter.
Edit: I want to add that multi-tenancy in my database is archived by database separation(Tenant1 uses DB1, Tenant2 uses DB2 etc.). The tenant variable is an enum and not included in my entities.
Edit2: I implemented my own solution. I use the tenants as indexes and use my ContextHolder from DataSource Routing to route to the correct tenant index. For that I had to do some changes the elasticsearchTemplate in the generated classes of the package "<my.package.name>.repository.search".
It might not be the most efficient way to reach multi tenancy with elasticsearch, but it doesn't need much configuration.
Here is the code:
public interface ProductSearchRepository extends ElasticsearchRepository<Product, Long>, ProductSearchRepositoryInternal {}
interface ProductSearchRepositoryInternal {
Stream<Product> search(String query);
Stream<Product> search(Query query);
void index(Product entity);
}
class ProductSearchRepositoryInternalImpl implements ProductSearchRepositoryInternal {
private final ElasticsearchRestTemplate elasticsearchTemplate;
private final ProductRepository repository;
ProductSearchRepositoryInternalImpl(ElasticsearchRestTemplate elasticsearchTemplate, ProductRepository repository) {
this.elasticsearchTemplate = elasticsearchTemplate;
this.repository = repository;
}
#Override
public Stream<Product> search(String query) {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryStringQuery(query));
return search(nativeSearchQuery);
}
#Override
public Stream<Product> search(Query query) {
return elasticsearchTemplate.search(query, Product.class, IndexCoordinates.of(TenantContextHolder.getTenantContext().getTenant())).map(SearchHit::getContent).stream();
}
#Override
public void index(Product entity) {
repository.findById(entity.getId()).ifPresent(t -> elasticsearchTemplate.save(t, IndexCoordinates.of(TenantContextHolder.getTenantContext().getTenant())));
}
}
Edit3: Since people might not know where ".getTenant()" comes from, I'll show my tenant enumeration:
public enum Tenant {
TENANTA("tenant_a"),
TENANTB("tenant_b");
String tenant;
Tenant(String name) {
this.tenant=name;
}
public String getTenant() {
return this.tenant;
}
}
Edit4: My solution is not working as planned. I will give an update once I found a better and more robust solution.
Edit5: I have found out how to implement tenant-based routing. First you have to add the following Annotation to your entities:
#org.springframework.data.elasticsearch.annotations.Routing(value = "tenant")
In my case I had to include the enum "Tenant" into my entities along with the getter and setter:
#Transient
private Tenant tenant;
public Tenant getTenant() {
return tenant;
}
public void setTenant(Tenant tenant) {
this.tenant = tenant;
}
Then I have to set the tenant during the processing of a REST request before it gets indexed by elasticsearchtemplate:
entity.setTenant(TenantContextHolder.getTenantContext());
As for the search function, I had to add a term query as a filter to enable routing:
#Override
public Stream<Product> search(String query) {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryStringQuery(query)
, QueryBuilders.termQuery("_routing", TenantContextHolder.getTenantContext()));
return search(nativeSearchQuery);
}
The method "setRoute(String route)" of "nativeSearchQuery" either does not work in my case or I didn't understand how it works.
I have successfully tested this implementation with GET and POST requests. Currently I have a problem with elasticsearch overwriting data if the id of the entity from one tenant I want to save is the same id as another entity with a different tenant.
After some trial and error, I found a solution to the overwriting problem and successfully completed and tested my implementation of tenant-based routing. Here is the code:
Product.java
import java.io.Serializable;
import javax.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.springframework.data.elasticsearch.annotations.Field;
#Entity
#Table(name = "product")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
#org.springframework.data.elasticsearch.annotations.Document(indexName = "product")
#SuppressWarnings("common-java:DuplicatedBlocks")
#org.springframework.data.elasticsearch.annotations.Routing(value = "tenant")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Transient
private Tenant tenant;
#Transient
#Field(name = "elastic_id")
#org.springframework.data.annotation.Id
private String elasticsearchId;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
#Field("postgres_id")
private Long id;
//Getters, Setters and other variables
}
ProductSearchRepository
public interface ProductSearchRepository extends ElasticsearchRepository<Product, Long>, ProductSearchRepositoryInternal {}
interface ProductSearchRepositoryInternal {
Stream<Product> search(String query);
Stream<Product> search(Query query);
void index(Product entity);
Product save(Product entity);
void deleteById(Long id);
}
#Transactional
class ProductSearchRepositoryInternalImpl implements ProductSearchRepositoryInternal {
private final ElasticsearchRestTemplate elasticsearchTemplate;
private final ProductRepository repository;
ProductSearchRepositoryInternalImpl(ElasticsearchRestTemplate elasticsearchTemplate, ProductRepository repository) {
this.elasticsearchTemplate = elasticsearchTemplate;
this.repository = repository;
}
#Override
public Stream<Product> search(String query) {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryStringQuery(query)
, QueryBuilders.termQuery("_routing", TenantContextHolder.getTenantContext()));
nativeSearchQuery.setMaxResults(30);
return search(nativeSearchQuery);
}
#Override
public Stream<Product> search(Query query) {
return elasticsearchTemplate.search(query, Product.class).map(SearchHit::getContent).stream();
}
#Override
public void index(Product entity) {
entity.setTenant(TenantContextHolder.getTenantContext());
repository.findById(Long.valueOf(entity.getId())).ifPresent(t -> {
entity.setElasticsearchId(entity.getTenant()+String.valueOf(entity.getId()));
elasticsearchTemplate.save(t);
});
}
#Override
public Product save(Product entity) {
entity.setTenant(TenantContextHolder.getTenantContext());
entity.setElasticsearchId(entity.getTenant()+String.valueOf(entity.getId()));
return elasticsearchTemplate.save(entity);
}
#Override
public void deleteById(Long id) {
elasticsearchTemplate.delete(TenantContextHolder.getTenantContext() + String.valueOf(id), Product.class);
}
}

Jackson: Multiple Serializers on the same entity when differents Rest EndPoint are called

I'm trying to avoid using the DTO antipattern when different EndPoint are called, where each returns a distinct representation of the same entity. I'd like to take advantage of the serialization that Jackson performs when I return the entity in the Rest EndPoint. This means that serialization is only done once and not twice as it would be with a DTO (entity to DTO and DTO to Json):
EndPoints example:
#GetMapping("/events")
public ResponseEntity<List<Event>> getAllEvents(){
try {
List<Event> events = (List<Event>) eventsRepository.findAll();
return new ResponseEntity<List<Event>>(
events, HttpStatus.OK);
}catch(IllegalArgumentException e) {
return new ResponseEntity<List<Event>>(HttpStatus.BAD_REQUEST);
}
}
#GetMapping("/events/{code}")
public ResponseEntity<Event> retrieveEvent(#PathVariable String code){
Optional<Event> event = eventsRepository.findByCode(code);
return event.isPresent() ?
new ResponseEntity<Event>(event.get(), HttpStatus.OK) :
new ResponseEntity<Event>(HttpStatus.BAD_REQUEST);
}
Serializer (class that extends of StdSerializer):
#Override
public void serialize(Event value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
if(firstRepresentation) {
//First Representation
gen.writeStartObject();
gen.writeNumberField("id", value.getId());
gen.writeObjectField("creation", value.getCreation());
gen.writeObjectFieldStart("event_tracks");
for (EventTrack eventTrack : value.getEventsTracks()) {
gen.writeNumberField("id", eventTrack.getId());
gen.writeObjectField("startTime", eventTrack.getStartTime());
gen.writeObjectField("endTime", eventTrack.getEndTime());
gen.writeNumberField("priority", eventTrack.getPriority());
gen.writeObjectFieldStart("user");
gen.writeNumberField("id", eventTrack.getUser().getId());
gen.writeEndObject();
gen.writeObjectFieldStart("state");
gen.writeNumberField("id", eventTrack.getState().getId());
gen.writeStringField("name", eventTrack.getState().getName());
gen.writeEndObject();
}
gen.writeEndObject();
gen.writeEndObject();
}else if(secondRepresentation) {
//Second Representation
}
}
Entity:
#JsonSerialize(using = EventSerializer.class)
#RequiredArgsConstructor
#Getter
#Setter
public class Event implements Comparable<Event>{
private Long id;
#JsonIgnore
private String code;
private Timestamp creation;
#NonNull
private String description;
#JsonUnwrapped
#NonNull
private EventSource eventSource;
#NonNull
private String title;
#NonNull
private Category category;
#NonNull
#JsonProperty("event_tracks")
private List<EventTrack> eventsTracks;
#JsonProperty("protocol_tracks")
private List<ProtocolTrack> protocolTracks;
public void addEventTrack(#NonNull EventTrack eventTracks) {
eventsTracks.add(eventTracks);
}
#JsonIgnore
public EventTrack getLastEventTrack() {
return eventsTracks.get(eventsTracks.size() - 1);
}
#JsonIgnore
public int getLastPriority() {
return getLastEventTrack().getPriority();
}
public void generateUUIDCode() {
this.code = UUID.randomUUID().toString();
}
#Override
public int compareTo(Event o) {
return this.getLastPriority() - o.getLastPriority();
}
}
So, so far I have been able to serialize a representation type with a class that extend of StdDeserializer, but this doesn't give me the flexibility to extend the representations of the same entity attributes in multiple ways. Although I've tried it with Json annotations, but I realize that the more representations the entity class has, it can get very complex, something that it should be simple. Maybe some idea how I could do it.
Thank you.
If you want to define multiple representations of the same bean you could use Jackson JsonView.
With json views you can set different strategies to define which property will be serialized in the response and so use different views by endpoint.
Documentation here : https://www.baeldung.com/jackson-json-view-annotation
Just don't forget that you doing REST here....avoid expose too many representations of the same resource

Spring Data REST Custom Resource URI works for String but not Long

I have a model:
public class MyModel {
#Id private Long id;
private Long externalId;
// Getters, setters
}
I'd like to use externalId as my resource identifier:
#Configuration
static class RepositoryEntityLookupConfig extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration configuration) {
configuration
.withEntityLookup()
.forRepository(MyRepository.class, MyModel::getExternalId, MyRepository::findByExternalId);
}
}
If externalId is a String, this works fine. But since it's a number (Long)
public interface MyRepository extends JpaRepository<MyModel, Long> {
Optional<MyModel> findByExternalId(#Param("externalId") Long externalId);
}
when invoking: /myModels/1 I get:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long
at org.springframework.data.rest.core.config.EntityLookupConfiguration$RepositoriesEntityLookup.lookupEntity(EntityLookupConfiguration.java:213) ~[spring-data-rest-core-2.6.4.RELEASE.jar:na]
at org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory$UnwrappingRepositoryInvoker.invokeFindOne(UnwrappingRepositoryInvokerFactory.java:130) ~[spring-data-rest-core-2.6.4.RELEASE.jar:na]
at org.springframework.data.rest.webmvc.RepositoryEntityController.getItemResource(RepositoryEntityController.java:524) ~[spring-data-rest-webmvc-2.6.4.RELEASE.jar:na]
at org.springframework.data.rest.webmvc.RepositoryEntityController.getItemResource(RepositoryEntityController.java:335) ~[spring-data-rest-webmvc-2.6.4.RELEASE.jar:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
...
A separate custom EntityLookupSupport<MyModel> component class works.
Am I missing something to get it working for Long using method references in my RepositoryRestConfigurerAdapter?
Try to add this to your RepositoryEntityLookupConfig class:
#Override
public void configureConversionService(ConfigurableConversionService conversionService) {
conversionService.addConverter(String.class, Long.class, Long::parseLong);
super.configureConversionService(conversionService);
}
Do you really need to set configuration by yourself ? You could try to use spring-boot auto-configuration by adding #RepositoryRestResource annotation
#RepositoryRestResource(collectionResourceRel = "myModels", path = "myModels")
public interface MyRepository extends JpaRepository<MyModel, Long> {
Optional<MyModel> findByExternalId(#Param("externalId") Long externalId);
}
Also add #Entity on your model class
#Entity
public class MyModel {
#Id
private Long id;
#Column(name = "EXTERNAL_ID")
// Column annotation is not required if you respect case-sensitive
private Long externalId;
// Getters, setters
}
Apparently, the default BackendIdConverter (see DefaultIdConverter) does nothing with ID conversion and on the other hand Spring Data Rest cannot use the repository ID type. So, you have to either convert it yourself or configure your custom ID converter bean, for example:
#Bean
public BackendIdConverter myModelBackendIdConverter() {
return new BackendIdConverter() {
#Override
public Serializable fromRequestId(final String id, final Class<?> entityType) {
return Optional.ofNullable(id).map(Long::parseLong).orElse(null);
}
#Override
public boolean supports(final Class<?> delimiter) {
return MyModel.class.isAssignableFrom(delimiter);
}
#Override
public String toRequestId(final Serializable id, final Class<?> entityType) {
return Optional.ofNullable(id).map(Object::toString).orElse(null);
}
};
}
See also:
BackendIdHandlerMethodArgumentResolver
#BackendId
The signature of the method you are trying to call seems to be:
forRepository(Class<R> type, Converter<T,ID> identifierMapping,
EntityLookupRegistrar.LookupRegistrar.Lookup<R,ID> lookup)
I don't see how MyModel::getExternalId can be doing the necessary conversion.
I would try something like the following:
#Configuration
static class RepositoryEntityLookupConfig extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration configuration) {
configuration
.withEntityLookup()
.forRepository(MyRepository.class, Long::parseLong, MyRepository::findByExternalId);
}
}

Spring Data Rest Repository with abstract class / inheritance

I can't get Spring Data Rest with class inheritance working.
I'd like to have a single JSON Endpoint which handles all my concrete classes.
Repo:
public interface AbstractFooRepo extends KeyValueRepository<AbstractFoo, String> {}
Abstract class:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = MyFoo.class, name = "MY_FOO")
})
public abstract class AbstractFoo {
#Id public String id;
public String type;
}
Concrete class:
public class MyFoo extends AbstractFoo { }
Now when calling POST /abstractFoos with {"type":"MY_FOO"}, it tells me: java.lang.IllegalArgumentException: PersistentEntity must not be null!.
This seems to happen, because Spring doesn't know about MyFoo.
Is there some way to tell Spring Data REST about MyFoo without creating a Repository and a REST Endpoint for it?
(I'm using Spring Boot 1.5.1 and Spring Data REST 2.6.0)
EDIT:
Application.java:
#SpringBootApplication
#EnableMapRepositories
public class Application {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
I'm using Spring Boot 1.5.1 and Spring Data Release Ingalls.
KeyValueRepository doesn't work with inheritance. It uses the class name of every saved object to find the corresponding key-value-store. E.g. save(new Foo()) will place the saved object within the Foo collection. And abstractFoosRepo.findAll() will look within the AbstractFoo collection and won't find any Foo object.
Here's the working code using MongoRepository:
Application.java
Default Spring Boot Application Starter.
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
AbstractFoo.java
I've tested include = JsonTypeInfo.As.EXISTING_PROPERTY and include = JsonTypeInfo.As.PROPERTY. Both seem to work fine!
It's even possible to register the Jackson SubTypes with a custom JacksonModule.
IMPORTANT: #RestResource(path="abstractFoos") is highly recommended. Else the _links.self links will point to /foos and /bars instead of /abstractFoos.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = Foo.class, name = "MY_FOO"),
#JsonSubTypes.Type(value = Bar.class, name = "MY_Bar")
})
#Document(collection="foo_collection")
#RestResource(path="abstractFoos")
public abstract class AbstractFoo {
#Id public String id;
public abstract String getType();
}
AbstractFooRepo.java
Nothing special here
public interface AbstractFooRepo extends MongoRepository<AbstractFoo, String> { }
Foo.java & Bar.java
#Persistent
public class Foo extends AbstractFoo {
#Override
public String getType() {
return "MY_FOO";
}
}
#Persistent
public class Bar extends AbstractFoo {
#Override
public String getType() {
return "MY_BAR";
}
}
FooRelProvider.java
Without this part, the output of the objects would be separated in two arrays under _embedded.foos and _embedded.bars.
The supports method ensures that for all classes which extend AbstractFoo, the objects will be placed within _embedded.abstractFoos.
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class FooRelProvider extends EvoInflectorRelProvider {
#Override
public String getCollectionResourceRelFor(final Class<?> type) {
return super.getCollectionResourceRelFor(AbstractFoo.class);
}
#Override
public String getItemResourceRelFor(final Class<?> type) {
return super.getItemResourceRelFor(AbstractFoo.class);
}
#Override
public boolean supports(final Class<?> delimiter) {
return AbstractFoo.class.isAssignableFrom(delimiter);
}
}
EDIT
Added #Persistent to Foo.java and Bar.java. (Adding it to AbstractFoo.java doesn't work). Without this annotation I got NullPointerExceptions when trying to use JSR 303 Validation Annotations within inherited classes.
Example code to reproduce the error:
public class A {
#Id public String id;
#Valid public B b;
// #JsonTypeInfo + #JsonSubTypes
public static abstract class B {
#NotNull public String s;
}
// #Persistent <- Needed!
public static class B1 extends B { }
}
Please see the discussion in this resolved jira task for details of what is currently supported in spring-data-rest regarding JsonTypeInfo. And this jira task on what is still missing.
To summarize - only #JsonTypeInfo with include=JsonTypeInfo.As.EXISTING_PROPERTY is working for serialization and deserialization currently.
Also, you need spring-data-rest 2.5.3 (Hopper SR3) or later to get this limited support.
Please see my sample application - https://github.com/mduesterhoeft/spring-data-rest-entity-inheritance/tree/fixed-hopper-sr3-snapshot
With include=JsonTypeInfo.As.EXISTING_PROPERTY the type information is extracted from a regular property. An example helps getting the point of this way of adding type information:
The abstract class:
#Entity #Inheritance(strategy= SINGLE_TABLE)
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,
include=JsonTypeInfo.As.EXISTING_PROPERTY,
property="type")
#JsonSubTypes({
#Type(name="DECIMAL", value=DecimalValue.class),
#Type(name="STRING", value=StringValue.class)})
public abstract class Value {
#Id #GeneratedValue(strategy = IDENTITY)
#Getter
private Long id;
public abstract String getType();
}
And the subclass:
#Entity #DiscriminatorValue("D")
#Getter #Setter
public class DecimalValue extends Value {
#Column(name = "DECIMAL_VALUE")
private BigDecimal value;
public String getType() {
return "DECIMAL";
}
}

No composite key property found for type error in Spring JPA2

I have an error in spring JPA
org.springframework.data.mapping.PropertyReferenceException: No property CompanyId found for type CompanyUserDetail!
#Embeddable
public class CompanyUserKey implements Serializable {
public CompanyUserKey() {
}
#Column(name = "company_id")
private UUID companyId;
#Column(name = "user_name")
private String userName;
public UUID getCompanyId() {
return companyId;
}
public void setCompanyId(UUID companyId) {
this.companyId = companyId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
#Entity
#Table(name = "company_user_detail")
public class CompanyUserDetail {
#EmbeddedId
CompanyUserKey companyUserkey;
public CompanyUserKey getCompanyUserkey() {
return companyUserkey;
}
public void setCompanyUserkey(CompanyUserKey companyUserkey) {
this.companyUserkey = companyUserkey;
}
}
I am trying to access below method Service layer
#Component
public interface CompanyUserRepository extends JpaRepository<CompanyUserDetail, CompanyUserKey> {
public List<CompanyUserDetail> findByCompanyId(UUID companyId);
}
How can I achieve this ?
Thanks
Since in java model your CompanyUserKey is a property in the CompanyUserDetail class, I believe you should use full path (companyUserkey.companyId) to reach companyId:
public List<CompanyUserDetail> findByCompanyUserkeyCompanyId(UUID companyId);
Also note that you have a naming inconsistency: field in CompanyUserDetail is named companyUserkey instead of companyUserKey.
Assuming you are not using spring-data-jpa's auto generated implementations, your method contents might look something like the following:
FROM CompanyUserDetail c WHERE c.companyUserKey.companyId = :companyId
Now simply provide that query to the EntityManager
entityManager.createQuery( queryString, CompanyUserDetail.class )
.setParameter( "companyId", companyId )
.getResultList();
The key points are:
Query uses a named bind parameter called :companyId (not the leading :).
Parameter values are bound in a secondary step using setParameter method variants.
createQuery uses a second argument to influence type safety so that the return value from getResultList is a List<CompanyUserDetail> just like you requested.
Looking at spring-data-jpa's implementation however, I suspect it could look like this:
public interface CustomerUserRepository
extends JpaRepository<CompanyUserDetail, CompanyUserKey> {
#Query("select c FROM CompanyUserDetail c WHERE c.companyUserKey.companyId = :companyId")
List<CompanyUserDetail> findByCompanyId(#Param("companyId") UUID companyId);
}

Resources