Let's say I've got the following code:
Entity getEntity(GUID entityId) {
Entity entity = entityLRUCache.get(entityId);
if (entity == null) {
entity = longLoadFromDatabase(entityId);
entityLRUCache.put(entityId, entity);
}
return entity;
}
Thanks to #BenManes I can use Caffeine framework to solve cache stampede:
Entity getEntity(GUID entityId) {
return entityCache.get(entityId, this::longLoadFromDatabase);
}
But now method longLoadFromDatabase returns a Single<Entity> instead of Entity itself (that 2nd argument for get is a mapper from int -> Entity), so the previous solutions won't work anymore.
You can use a SingleSubject as a placeholder in a concurrent map:
ConcurrentMap<GUID, SingleSubject<Entity>> map = ...
public Single<Entity> getEntity(GUID guid) {
SingleSubject<Entity> e = map.get(guid);
if (e == null) {
e = SingleSubject.create();
SingleSubject<Entity> f = map.putIfAbsent(guid, e);
if (f == null) {
longLoadFromDatabase(guid).subscribe(e);
} else {
e = f;
}
}
return e;
}
Combining #akarnokd suggestion with Caffeine's async support, you can use AsyncLoadingCache with Rx's converters.
AsyncLoadingCache<Integer, Entity> cache = Caffeine.newBuilder()
.buildAsync((key, executor) -> SingleInterop.get()
.apply(longLoadFromDatabase(key)).toCompletableFuture());
...
return SingleInterop.fromFuture(cache.get(123));
The cache will automatically remove the entry if the future fails with an exception or resolves to a null value.
Related
I'm having trouble with understanding how to achieve my goal with reactive approach.
Let's assume that I have a Controller, that will return Flux:
#PostMapping(value = "/mutation/stream/{domainId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Mutation> getMutationReactive(#RequestBody List<MutationRequest> mutationRequests, #PathVariable Integer domainId) {
return mutationService.getMutations(mutationRequests, domainId);
}
In service, currently with .subscribeOn(Schedulers.boundedElastic()), because it calls for a blocking code that is wrapped into a Callable.
public Flux<Mutation> getMutations(List<MutationRequest> mutationRequests, int domainId) {
return Flux.fromIterable(mutationRequests)
.subscribeOn(Schedulers.boundedElastic())
.flatMap(mutationRequest -> getMutation(mutationRequest.getGameId(), mutationRequest.getTypeId(), domainId));
}
getMutation() with blocking calls, currently wrapped into a Callable:
private Mono<Mutation> getMutation(int gameId, int typeId, int domainId) {
return Mono.fromCallable(() -> {
Mutation mutation = mutationProvider.findByGameIdAndTypeId(gameId, typeId).block(); // mutationProvider.findByGameIdAndTypeId() returns Mono<Mutation>
if (mutation == null) {
throw new RuntimeException("Mutation was not found by gameId and typeId");
}
State state = stateService.getStateByIds(mutation.getId()), domainId).blockFirst(); //stateService.getStateByIds() returns Mono<State>
if (state == null || state.getValue() == null) {
log.info("Requested mutation with gameId[%s] typeId[%s] domainId[%s] is disabled. Value is null.".formatted(gameId, typeId, domainId));
return null;
}
mutation.setTemplateId(state.getTemplateId());
return (mutation);
});
}
How do I approach the getMutation() function to use reactive streams, instead of using .block() methods inside a Callable?
Basically, I first need to retrieve Mutation from DB -> then using ID of mutation, get its state from other service -> then if state and its value are not null, set templateId of state to mutation and return, or return null.
I've tried something like this:
private Mono<Mutation> getMutation(int gameId, int typeId, int domainId) {
return mutationProvider.findByGameIdAndTypeId(gameId, typeId)
.flatMap(mutation -> {
stateService.getStatesByIds(mutation.getId(), domainId).flatMap(state -> {
if (state != null && state.getValue() != null) {
mutation.setTemplateId(state.getTemplateId());
}
//TODO if state/value is null -> need to propagate further to return null instead of mutation...
return Mono.justOrEmpty(state);
});
return Mono.just(mutation);
});
}
But it's obviously incorrect, nothing is subscribed to stateService.getStatesByIds(mutation.getId()), domainId)
AND
I would like to return a null if the retrieved state of mutation or its value are null.
You are ignoring the value of the inner flatMap hence the warning.
Without trying you need something like this
private Mono<Mutation> getMutation(int gameId, int typeId, int domainId) {
return mutationProvider.findByGameIdAndTypeId(gameId, typeId)
.flatMap(mutation -> {
return stateService.getStatesByIds(mutation.getId(), domainId).flatMap(state -> {
if (state != null && state.getValue() != null) {
mutation.setTemplateId(state.getTemplateId());
return Mono.just(mutation);
}
return Mono.empty();
});
});
}
Although not sure if you could rewrite the outer flatMap not to a regular map instead and you might want to use filter and defaultIfEmpty with that as well
private Mono<Mutation> getMutation(int gameId, int typeId, int domainId) {
return mutationProvider.findByGameIdAndTypeId(gameId, typeId)
.flatMap(mutation -> {
return stateService.getStatesByIds(mutation.getId(), domainId)
.filter(state -> state != null && state.getValue() != null)
.flatMap(state -> {
mutation.setTemplateId(state.getTemplateId());
return Mono.just(mutation);})
.defaultIfEmpty(Mono.empty());
}
This is just from the top of my head and I have no idea what some of the return types are here (Flux or Mono) for your own APIs.
I have similar class structure.
class Request {
Level1 level1;
}
class Level1 {
Level2 level2;
String data;
}
class Level2 {
Level3 level3;
String data;
}
class Level3 {
Level4 level4;
String data;
}
class Level4 {
String data;
}
Request r = new Request();
r.level1 = new Level1();
r.level1.level2 = new Level2();
r.level1.level2.level3 = null;//new Level3();
//r.level1.level2.level3.level4 = new Level4();
//r.level1.level2.level3.level4.data = "level4Data";
and to get data from nested fields I do following
Benefit of using Optional being I don't have to worry about checking null at each level in object hierarchy
String level4Data = Optional.ofNullable(r)
.map(req -> req.level1)
.map(l1 -> l1.level2)
.map(l2 -> l2.level3)
.map(l3 -> l3.level4)
.map(l4 -> l4.data)
.orElse(null);
System.out.println("level4Data: " + level4Data);
but again if I want to log reason behind level4Data being null, I have to do following/I don't any better
if (level4Data == null) {
if (r == null) System.out.println("request was null");
else if (r.level1 == null) System.out.println("level1 was null");
else if (r.level1.level2 == null) System.out.println("level2 was null");
else if (r.level1.level2.level3 == null) System.out.println("level3 was null");
else if (r.level1.level2.level3.level4 == null) System.out.println("level4 was null");
else if (r.level1.level2.level3.level4.data == null) System.out.println("level4.data was null");
}
is there more elegant/efficient way of doing this as it defeats benefits of using Optional in first place
Thank you for your time and inputs
Optional doesn't have a peek method like in Stream API.
For your use case, you can write a wrapper that will do the additional job:
// logging wrapper
static <T, R> Function<T, R> logIfNull(Function<? super T, ? extends R> function, String message) {
return input -> {
R result;
if ((result = function.apply(input)) == null)
System.out.println("logIfNull :: Null found. " + message);
return result;
};
}
// usage
String level4Data = Optional.ofNullable(r)
.map(logIfNull(req -> req.level1, "req.level1"))
.map(logIfNull(l1 -> l1.level2, "l1.level2"))
.map(logIfNull(l2 -> l2.level3, "l2.level3"))
.map(logIfNull(l3 -> l3.level4, "l3.level4"))
.map(logIfNull(l4 -> l4.data, "l4.data"))
.orElse(null);
I have the below Java 11 method which is invoked by the controller where ID is the required param and status,version are optional params. I had to write multiple repository methods to fetch the record based on those params. Am wondering is there a better/effiecient way to refactor this method with out the if/else ladder?
#Override
#Transactional(transactionManager = "customTransactionManager")
public Optional<String> getInformation(UUID id, Status status, Long version) {
try {
Preconditions.checkNotNull(id, ID_MUST_BE_NOT_NULL_MSG);
if (status != null && version != null) {
return repository.findByIdAndVersionAndStatus(id, version, status);
} else if (status != null) {
return repository.findFirstByIdAndStatus(id, status);
} else if (version != null) {
return repository.findFirstByIdAndVersion(id, version);
} else {
return repository.findFirstByIdOrderByIdDesc(id);
}
} catch (Exception e) {
log.error(e);
throw new CustomException(MessageFormat.format(PUBLIC_ERROR_MESSAGE, id));
}
}
You could use Specifications for that:
private Specification<YourEntity> toSpecification(UUID id, Status status, Long version) {
return (root, query, builder) -> {
Set<Predicate> predicates = new HashSet<>();
predicates.add(builder.equal(root.get("id"), id));
if (status != null) predicates.add(builder.equal(root.get("status"), status));
if (version != null) predicates.add(builder.equal(root.get("version"), version));
return builder.and(predicates.toArray(Predicate[]::new));
};
}
If you let your repository extend JpaSpecificationExecutor you can use the build specification object like so:
Specification<YourEntity> specification = toSpecification(id, status, version);
Optional<YourEntity> result = repository.findOne(specification);
When using Hibernate Metamodel Generator you can also write builder.equal(YourEntity_.id, id) instead of builder.equal(root.get("id"), id).
In addition to the accepted answer, I find Query By Examples much more intuitive and simple.
https://www.baeldung.com/spring-data-query-by-example would be a good start.
It basically creates a query based on non-null fields from your jpa entity.
I'm using the webflux framework for spring boot, the behavior I'm trying to implement is creating a new customer in the database, if it does not already exist (throw an exception if it does)
and also maintain another country code database (if the new customer is from a new country, add to the database, if the country is already saved, use the old information)
This is the function in the service :
public Mono<Customer> createNewCustomer(Customer customer) {
if(!customer.isValid()) {
return Mono.error(new BadRequestException("Bad email or birthdate format"));
}
Mono<Customer> customerFromDB = customerDB.findByEmail(customer.getEmail());
Mono<Country> countryFromDB = countryDB.findByCountryCode(customer.getCountryCode());
Mono<Customer> c = customerFromDB.zipWith(countryFromDB).doOnSuccess(new Consumer<Tuple2<Customer, Country>>() {
#Override
public void accept(Tuple2<Customer, Country> t) {
System.err.println("tuple " + t);
if(t == null) {
countryDB.save(new Country(customer.getCountryCode(), customer.getCountryName())).subscribe();
customerDB.save(customer).subscribe();
return;
}
Customer cus = t.getT1();
Country country = t.getT2();
if(cus != null) {
throw new CustomerAlreadyExistsException();
}
if(country == null) {
countryDB.save(new Country(customer.getCountryCode(), customer.getCountryName())).subscribe();
}
else {
customer.setCountryName(country.getCountryName());
}
customerDB.save(customer).subscribe();
}
}).thenReturn(customer);
return c;
}
My problem is, the tuple returns null if either country or customer are not found, while I need to know about them separately if they exist or not, so that I can save to the database correctly.
country == null is never true
I also tried to use customerFromDB.block() to get the actual value but I receive an error that it's not supported, so I guess that's not the way
Is there anyway to do two queries to get their values?
Solved it with the following solution:
public Mono<Customer> createNewCustomer(Customer customer) {
if(!customer.isValid()) {
return Mono.error(new BadRequestException("Bad email or birthdate format"));
}
return customerDB.findByEmail(customer.getEmail())
.defaultIfEmpty(new Customer("empty", "", "", "", "", ""))
.flatMap(cu -> {
if(!cu.getEmail().equals("empty")) {
return Mono.error(new CustomerAlreadyExistsException());
}
return countryDB.findByCountryCode(customer.getCountryCode())
.defaultIfEmpty(new Country(customer.getCountryCode(), customer.getCountryName()))
.flatMap(country -> {
customer.setCountryName(country.getCountryName());
customerDB.save(customer).subscribe();
countryDB.save(country).subscribe();
return Mono.just(customer);});
});
}
Instead of doing both queries simulatneaously, I queried for one result and then queries for the next, I think this is the reactive way of doing it, but I'm open for corrections.
I am reading data from an external organisation using Oracle.DataAccess through the Oracle10g provider. One of the tables has a composite id made up of these fields.
course:
institutioncode: "X11"
coursecode: "N100"
campuscode: "A"
entryyear: 2011
entrymonth: 10
The problem is that the campus code is allowed to be null by the external provider instead of empty. This leads to nHibernate returning collections that contain null references instead of course entities.
Other domain objects will use these fields to refer to this course entity as well, so this is actually used as a key and I can't easily remap to use a surrogate key.
From the source in tag 3.1.0GA, the check that is causing this behavior can be found in Nhibernate.Type.ComponentType.Hydrate(IDataReader rs, string[] names, ISessionImplementor session, object owner). This is always refusing the possibility that a key-property could be null. Could this change to make nullability an option on key-property and key-reference properties?
Failing that, how would you recommend reading this data directly with nHibernate?
NULL values in properties are not supported by design.
There are two ways to deal with this:
Import the data instead of using it raw from the source, adding a proper surrogate key.
Handle that entity without NHibernate.
ok my first comment didnt worked out on References (ManyToOne). So here my alternative solution: a usertype to work around the check.
class CourseMap : ClassMap<Course>
{
public CourseMap()
{
CompositeId()
.KeyProperty(c => c.InstitutionCode)
.KeyProperty(c => c.CourseCode)
.KeyProperty(c => c.CampusCode, key => key.Type(typeof(MyUserType)))
.KeyProperty(c => c.EntryYear)
.KeyProperty(c => c.EntryMonth);
}
}
class MyUserType : IUserType
{
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object DeepCopy(object value)
{
return value;
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public new bool Equals(object x, object y)
{
return object.Equals(x, y);
}
public int GetHashCode(object x)
{
return (x == null) ? 0 : x.GetHashCode();
}
public bool IsMutable
{
get { return false; }
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var value = NHibernateUtil.String.NullSafeGet(rs, names[0]);
return (value == null) ? string.Empty : value;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
string d = string.IsNullOrEmpty((string)value) ? null : (string)value;
NHibernateUtil.String.NullSafeSet(cmd, d, index);
}
public object Replace(object original, object target, object owner)
{
return DeepCopy(original);
}
public Type ReturnedType
{
get { return typeof(string); }
}
public SqlType[] SqlTypes
{
get { return new[] { SqlTypeFactory.GetString(100) }; }
}
}
class SomeEntityMap : ClassMap<SomeEntity>
{
public EntityMap()
{
Id(e => e.Id).GeneratedBy.Assigned();
References(e => e.Course)
.Columns("InstitutionCode", "CourseCode", "CampusCode", "EntryYear", "EntryMonth")
.Fetch.Join(); // important because we can't rely on values, NULL is invalid value
}
}