Daniel Bohry vor 1 Monat
Ursprung
Commit
d5148a2f9b

+ 2 - 0
build.gradle

@@ -20,8 +20,10 @@ dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
     implementation 'org.springframework.boot:spring-boot-starter-web'
     implementation 'org.springframework.boot:spring-boot-starter-actuator'
+    implementation 'org.springframework.boot:spring-boot-starter-cache'
 
     implementation 'com.github.f4b6a3:ulid-creator:5.2.3'
+    implementation 'com.github.ben-manes.caffeine:caffeine'
 
     testImplementation 'org.springframework.boot:spring-boot-starter-mongodb-test'
     testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'

+ 46 - 0
src/main/java/com/lhamacorp/knotes/config/CacheConfig.java

@@ -0,0 +1,46 @@
+package com.lhamacorp.knotes.config;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.caffeine.CaffeineCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+    @Bean
+    public CacheManager cacheManager() {
+        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
+
+        cacheManager.setCaffeine(Caffeine.newBuilder()
+                .initialCapacity(100)
+                .maximumSize(1000)
+                .expireAfterWrite(Duration.ofSeconds(10))
+                .recordStats());
+
+        return cacheManager;
+    }
+
+    @Bean("metadataCache")
+    public Caffeine<Object, Object> metadataCache() {
+        return Caffeine.newBuilder()
+                .initialCapacity(50)
+                .maximumSize(500)
+                .expireAfterWrite(Duration.ofSeconds(10))
+                .recordStats();
+    }
+
+    @Bean("contentCache")
+    public Caffeine<Object, Object> contentCache() {
+        return Caffeine.newBuilder()
+                .initialCapacity(20)
+                .maximumSize(100)
+                .expireAfterWrite(Duration.ofSeconds(30))
+                .recordStats();
+    }
+}

+ 4 - 1
src/main/java/com/lhamacorp/knotes/repository/NoteRepository.java

@@ -1,11 +1,11 @@
 package com.lhamacorp.knotes.repository;
 
-import com.lhamacorp.knotes.api.dto.NoteMetadata;
 import com.lhamacorp.knotes.domain.Note;
 import org.springframework.data.mongodb.repository.MongoRepository;
 import org.springframework.data.mongodb.repository.Query;
 import org.springframework.stereotype.Repository;
 
+import java.util.List;
 import java.util.Optional;
 
 @Repository
@@ -13,4 +13,7 @@ public interface NoteRepository extends MongoRepository<Note, String> {
 
     @Query(value = "{ '_id': ?0 }", fields = "{ 'createdAt': 1, 'modifiedAt': 1 }")
     Optional<Note> findMetadataProjectionById(String id);
+
+    @Query(value = "{ 'content': BinData(0, '') }", fields = "{ '_id': 1 }")
+    List<Note> findEmptyNotes();
 }

+ 16 - 10
src/main/java/com/lhamacorp/knotes/service/NoteService.java

@@ -8,6 +8,8 @@ import com.lhamacorp.knotes.exception.NotFoundException;
 import com.lhamacorp.knotes.repository.NoteRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
@@ -31,28 +33,38 @@ public class NoteService {
         return repository.existsById(id);
     }
 
+    @Cacheable(value = "note", key = "#id")
     public Note findById(String id) {
+        log.debug("Cache miss - fetching and decompressing note [{}]", id);
         return repository.findById(id)
                 .orElseThrow(() -> new NotFoundException("Note with id " + id + " not found!"));
     }
 
+    @Cacheable(value = "metadata", key = "#id")
     public NoteMetadata findMetadataById(String id) {
+        log.debug("Cache miss - fetching metadata for note [{}]", id);
         Note noteProjection = repository.findMetadataProjectionById(id)
                 .orElseThrow(() -> new NotFoundException("Note with id " + id + " not found!"));
         return NoteMetadata.from(noteProjection);
     }
 
+    @CacheEvict(value = {"note", "metadata"}, key = "#result.id")
     public Note save(String content) {
         Ulid id = UlidCreator.getUlid();
 
         log.info("Saving new note [{}]", id);
 
         Instant now = Instant.now();
-        return repository.save(new Note(id.toString(), content, now, now));
+        Note savedNote = repository.save(new Note(id.toString(), content, now, now));
+
+        log.debug("Evicting caches for new note [{}]", savedNote.id());
+        return savedNote;
     }
 
+    @CacheEvict(value = {"note", "metadata"}, key = "#id")
     public Note update(String id, String content) {
-        Note note = findById(id);
+        Note note = repository.findById(id)
+                .orElseThrow(() -> new NotFoundException("Note with id " + id + " not found!"));
 
         log.info("Updating note [{}]", id);
 
@@ -62,9 +74,8 @@ public class NoteService {
 
     @Scheduled(cron = ONCE_PER_DAY_AT_2AM)
     public void cleanup() {
-        List<Note> allNotes = repository.findAll();
-        List<String> ids = allNotes.stream()
-                .filter(this::isContentEmpty)
+        List<Note> emptyNotes = repository.findEmptyNotes();
+        List<String> ids = emptyNotes.stream()
                 .map(Note::id)
                 .toList();
 
@@ -74,9 +85,4 @@ public class NoteService {
         }
     }
 
-    private boolean isContentEmpty(Note note) {
-        String content = note.content();
-        return content == null || content.trim().isEmpty();
-    }
-
 }