Skip to content

Commit

Permalink
implement models data loader and setup some caching
Browse files Browse the repository at this point in the history
  • Loading branch information
bthreader committed Nov 26, 2023
1 parent 71a793e commit 0220595
Show file tree
Hide file tree
Showing 17 changed files with 192 additions and 84 deletions.
4 changes: 3 additions & 1 deletion service-course-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.netflix.graphql.dgs:graphql-dgs-extended-scalars'
testImplementation "org.mockito:mockito-core:3.+"
implementation 'com.graphql-java:graphql-java:21.2'
implementation 'com.graphql-java:graphql-java:21.2' // Locked version to avoid problems
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
}

generateJava {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package servicecourse;

import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfiguration {
public static final String BIKE_BRANDS = "bikeBrands";

@Bean
public CacheManager cacheManager() {
return new CaffeineCacheManager(BIKE_BRANDS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class ServiceCourseApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceCourseApplication.class, args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,32 @@

import com.netflix.graphql.dgs.*;
import lombok.RequiredArgsConstructor;
import org.dataloader.DataLoader;
import servicecourse.generated.types.BikeBrand;
import servicecourse.generated.types.CreateBikeBrandInput;
import servicecourse.generated.types.Model;
import servicecourse.services.bikebrands.BikeBrandsService;
import servicecourse.services.models.ModelsService;

import java.util.List;
import java.util.concurrent.CompletableFuture;

@DgsComponent
@RequiredArgsConstructor
public class BikeBrandsDataFetcher {
private final ModelsService modelsService;
private final BikeBrandsService bikeBrandsService;

/**
* This is an enhanced attribute. Services will not generate it ahead of time, unlike brand
* name. Therefore, if specified, it must be computed here.
* This is an enhanced attribute. Services will not generate it ahead of time (unlike brand
* name). Therefore, if specified by the user, it must be computed here.
* <p>
* A data loader is used to avoid sending multiple separate requests to the models service when
* handling a request that involves multiple bike brands (see {@link #bikeBrands()}).
*/
@DgsData(parentType = "BikeBrand", field = "models")
public List<Model> models(DgsDataFetchingEnvironment dfe) {
public CompletableFuture<List<Model>> models(DgsDataFetchingEnvironment dfe) {
BikeBrand bikeBrand = dfe.getSource();
// Data loader here
// Why?
// Because if some wind up merchant decides to ask for models on all bike brands
// after running bikeBrands query
// Models service will get pinged N+1 times
return modelsService.findByBrandName(bikeBrand.getName());
DataLoader<String, List<Model>> modelsDataLoader = dfe.getDataLoader("models");
return modelsDataLoader.load(bikeBrand.getName());
}

@DgsQuery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@

@Repository
public interface ModelRepository extends JpaRepository<ModelEntity, Long>, JpaSpecificationExecutor<ModelEntity> {
List<ModelEntity> findAllByBrandName(String brandName);
List<ModelEntity> findByBrandName(String brandName);

List<ModelEntity> findByBrandNameIn(List<String> brandNames);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.MalformedURLException;
import java.net.URL;

/** Converts a potentially null URL */
public class URLConverter implements AttributeConverter<URL, String> {
@Override
public String convertToDatabaseColumn(URL url) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package servicecourse.repo.specification;

import servicecourse.generated.types.StringFilterInput;

import java.util.Optional;
import java.util.function.Predicate;

public class StringFilterSpecification {
private StringFilterSpecification() { }

public static Predicate<String> from(StringFilterInput input) {
return arg -> Optional.ofNullable(input.getEquals()).map(arg::equals).orElse(true)
|| Optional.ofNullable(input.getContains()).map(arg::contains).orElse(true)
|| Optional.ofNullable(input.getIn()).map(in -> in.contains(arg)).orElse(true);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package servicecourse.services.bikebrands;

import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import servicecourse.CacheConfiguration;
import servicecourse.generated.types.BikeBrand;
import servicecourse.generated.types.CreateBikeBrandInput;
import servicecourse.repo.BikeBrandEntity;
Expand All @@ -13,10 +16,11 @@

@Service
@RequiredArgsConstructor
public class RelationalBikeBrandsService implements BikeBrandsService {
public class BikeBrandsServiceImpl implements BikeBrandsService {
private final BikeBrandRepository bikeBrandRepository;

@Override
@CacheEvict(value = CacheConfiguration.BIKE_BRANDS)
public BikeBrand createBikeBrand(CreateBikeBrandInput input) {
// Validate that the brand doesn't already exist
bikeBrandRepository.findById(input.getName())
Expand All @@ -26,14 +30,20 @@ public BikeBrand createBikeBrand(CreateBikeBrandInput input) {
}

@Override
@CacheEvict(value = CacheConfiguration.BIKE_BRANDS)
public String deleteBikeBrand(String name) {
return bikeBrandRepository.findById(name).map((entity) -> {
bikeBrandRepository.deleteById(name);
return name;
}).orElseThrow(Errors::newBikeBrandNotFoundError);
}

/**
* This method is cached. The cache is invalidated by {@link #createBikeBrand} and
* {@link #deleteBikeBrand}.
*/
@Override
@Cacheable(value = CacheConfiguration.BIKE_BRANDS, sync = true)
public List<BikeBrand> bikeBrands() {
return bikeBrandRepository.findAll()
.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

@Service
@RequiredArgsConstructor
public class RelationalBikesService implements BikesService {
public class BikesServiceImpl implements BikesService {
private final BikeRepository bikeRepository;
private final ModelRepository modelRepository;
private final GroupsetRespository groupsetRespository;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package servicecourse.services.groupsets;

public class GroupsetServiceImpl implements GroupsetService {
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
import java.util.Map;

public interface ModelsService {
/**
* @return all models belonging to the brand
*/
List<Model> findByBrandName(String brandName);

Model createModel(CreateModelInput input);

String deleteModel(String id);

/**
* @return a map; bike brand name -> models for that brand
*/
Map<String, List<Model>> modelsForBikeBrands(List<String> brandNames);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,26 @@

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import servicecourse.generated.types.BikeBrand;
import servicecourse.generated.types.CreateModelInput;
import servicecourse.generated.types.Model;
import servicecourse.repo.BikeBrandRepository;
import servicecourse.repo.ModelEntity;
import servicecourse.repo.ModelRepository;
import servicecourse.services.Errors;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class RelationalModelsService implements ModelsService {
public class ModelsServiceImpl implements ModelsService {
private final ModelRepository modelRepository;
private final BikeBrandRepository bikeBrandRepository;

@Override
public List<Model> findByBrandName(String brandName) {
return modelRepository.findAllByBrandName(brandName)
return modelRepository.findByBrandName(brandName)
.stream()
.map(ModelEntity::asModel)
.collect(Collectors.toList());
Expand Down Expand Up @@ -55,14 +53,10 @@ public String deleteModel(String id) {
}

public Map<String, List<Model>> modelsForBikeBrands(List<String> brandNames) {
Map<String, List<Model>> result = new HashMap<>();
result.put("hello", List.of(Model.newBuilder()
.name("hello")
.brand(BikeBrand.newBuilder()
.name("hello").build())
.modelYear(2022)
.build()));
return result;
return modelRepository.findByBrandNameIn(brandNames)
.stream()
.map(ModelEntity::asModel)
.collect(Collectors.groupingBy(m -> m.getBrand().getName()));
}
}

66 changes: 63 additions & 3 deletions service-course-api/src/main/resources/schema/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,40 @@ scalar Date

# Utils

enum Op {
OR
AND
}

input DateRangeFilterInput {
from: Date!
to: Date!
}

"""
Fields are combined with OR logic
"""
input StringFilterInput {
contains: String
in: [String!]
equals: String
}

input IntegerFilterInput {
lessThanOrEqualTo: Int
greaterThanOrEqualTo: Int
equals: Int
in: [Int!]
operator: Op
}

# Queries and mutations

type Query {
bikes(filter: BikesFilterInput): [Bike!]
bikes(filter: BikesFilterInput): [Bike!]!
"""
All bike brands
"""
bikeBrands: [BikeBrand!]!
# """
# Get the available days to book for a give bike for a given date range.
Expand All @@ -47,7 +72,7 @@ type Mutation {
deleteBikeBrand(name: String!): String
}

# Models
# Bike brands

type BikeBrand {
name: String!
Expand All @@ -58,6 +83,8 @@ input CreateBikeBrandInput {
name: String!
}

# Models

type Model {
id: ID!
name: String!
Expand All @@ -71,6 +98,15 @@ input CreateModelInput {
brandName: String!
}

"""
Fields are combined with AND logic
"""
input ModelFilterInput {
name: StringFilterInput
modelYear: IntegerFilterInput
brandName: String
}

# Bikes

type Bike {
Expand All @@ -85,18 +121,33 @@ input BikesFilterInput {
"""
If specified, return only bikes that are available in the provided date range
"""
availableDateRangeFilter: DateRangeFilterInput
availableDateRange: DateRangeFilterInput
"""
If specified, return only bikes whose model matches the criteria
"""
model: ModelFilterInput
"""
If specified, return only bikes with a groupset matching the criteria
"""
groupset: GroupsetFilterInput
size: StringFilterInput
}

input CreateBikeInput {
modelId: ID!
"""
There must be a groupset with this name already saved otherwise the mutation will fail
"""
groupsetName: String!
size: String!
heroImageUrl: Url
}

input UpdateBikeInput {
bikeId: ID!
"""
IF specified, there must be a groupset with this name already saved otherwise the mutation will fail
"""
groupsetName: String
heroImageUrl: Url
}
Expand All @@ -114,3 +165,12 @@ type Groupset {
brand: GroupsetBrand!
isElectronic: Boolean!
}

"""
Fields are combined with AND logic
"""
input GroupsetFilterInput {
name: String
brand: GroupsetBrand
isElectronic: Boolean
}
Loading

0 comments on commit 0220595

Please sign in to comment.