MongoDB — Redis — Spring Boot — Docker

Furkan Şahin Kulaksız
11 min readDec 9, 2023

--

GÖRSELDEKİ QR KODU KULLANARAK ILETISIM KANALLARIMA ULASABILIRSINIZ

Redis, genellikle Cache sistemlerinde kullanılan çok popüler bir tooldur. Genellikle, çünkü database olarak, pub/sub mekanizması olarak ve dahası için de kullanılabilir.

Cache ile alakalı daha fazla bilgi edinmek için buraya tıklayabilirsiniz.
Redis ile alakalı daha fazla bilgi edinmek için buraya tıklayabilirsiniz.

Ayrıca Redis’in resmi dokümantasyonunu okumak için buraya tıklayabilirsiniz.

Proje Gereksinimleri:
- Docker
- JDK 17+
- Spring Boot 3.0+
- Intellij IDE veya DataGrip (Optional)

DOCKER — MONGODB — SPRING BOOT

ADIM 1

Öncelikle MongoDB’yi docker’da ayağa kaldırmamız lazım. Bunun için docker daemon’u çalıştırdıktan sonra (ki docker desktop’u hatasız bir şekilde çalıştırınca docker daemon da çalışmış olur) aşağıdaki docker commandini terminale yazmamız yeterli olacaktır.

docker run -d -it -p 1905:27017  --name my-mongo-poc mongo:latest 

Bu komutu biraz tanıyalım.

  • docker run: Docker containerlarını başlatmak için kullanılan komut.
  • -d: Docker containerlarını arka planda (daemon modunda) çalıştırır. Bu, Docker konteynerinin arka planda çalışmasını sağlar.
  • -it: Etkileşimli bir terminal oturumu başlatır. Bu, Docker containerlarını bağlanmamıza ve komutları etkileşimli olarak çalıştırmanıza olanak tanır.
  • -p 1905:27017: Host işletim sistemi ve Docker containeri arasındaki bağlantı noktalarını eşleştirir. Yani, bu komut host işletim sistemi üzerinde 1905 numaralı portu Docker containerinin 27017 numaralı portuna yönlendirir. Bu, MongoDB'nin Docker containeri üzerinde 27017 numaralı portunda çalıştığı ve bu portun host işletim sistemi üzerinden erişilebilir olduğu anlamına gelir.
  • --name my-mongo-poc: Docker containerine bir isim verir. Bu durumda, containerimizin adı "my-mongo-poc" olarak ayarlanmıştır.
  • mongo:latest: Docker Hub'dan "mongo" adlı resmi MongoDB imajını kullanır. ":latest" etiketi, en son sürümü kullanmak istediğinizi belirtir.

ADIM 2

Bu adımdan sonra application.yml dosyamızı ayarlamamız lazım. Bunun için öncelik olarak server portunu ve mongodb için gerekli ayarlamaları yapmamız lazım.

#SERVER-CFG
server:
port: 8282

#MONGO-CFG
spring:
data:
mongodb:
host: localhost
port: 1905
database: blog-post-content

Normalde bizim application.yml dosyamızda docker olmadan bir mongodb bağlantısı sağladığımızda cfg’miz aşağıdaki gibi olmalı.

#MONGO-CFG
spring:
data:
mongodb:
host: localhost
uri: mongodb://localhost:27017/blog-post-content
database: blog-post-content

Ama docker işin içine girdiğinde işler biraz değişiyor. Öncelikle bizim docker’ımız localhostta çalışıyor. Bu yüzden host kısmına localhost yazmamız lazım.

Normalde MongoDB’nin default portu 27017. Ama biz docker komutunda 27107 portunu 1905 portuna açtık. Bu yüzden port yerine 1905 yazıyoruz.

Ve son olarak database’imizin adını verip yml dosyamızı sonlandırıyoruz.

ADIM 3

Bu adımda 2 tane çok basit MongoDB Collectionlarına karşılık gelecek modelimiz olduğunu düşünelim.

@Document
@Data
public class Post implements Serializable {

@Id
private String id;
private String title;
private String content;
@DBRef
private Writer writer;

}
@Document
@Data
public class Writer implements Serializable {

@Id
private String id;

@Indexed(unique = true)
private String userName;
private String name;
private String surname;
@DBRef
@JsonIgnore
private List<Post> postList;

}

Basit bir repository katmanı, service katmanı ve controller katmanları ile projemizi hızlıca bir requesti karşılayacak bir hale getirelim.

#WRITER-CONTROLLER
#-----------------

@RequestMapping("/writer")
@RestController
@RequiredArgsConstructor
public class WriterController {

private final WriterService writerService;

private final Logger LOG = LoggerFactory.getLogger(getClass());

@PostMapping("/add-writer")
public ResponseEntity<Writer> addPost(@RequestBody Writer writer) {
LOG.info("Saving Writer");
return ResponseEntity.ok().body(writerService.addWriter(writer));
}

}
#WRITER-SERVICE
#-----------------

@Service
@RequiredArgsConstructor
public class WriterService {

private final WriterRepository writerRepository;

public Writer addWriter(Writer writer) {
return writerRepository.save(writer);
}

}
#WRITER-REPOSITORY
#-----------------

public interface WriterRepository extends MongoRepository<Writer, String> {

}

ADIM 4

Artık uygulamamıza bir request atabiliriz.

localhost:8282/writer/add-writer adresine

Yukarıdaki gibi çok basit bir payload gönderdiğimiz zaman, 200 sonucunu görmemiz gerekir.

ADIM 5

Peki bu kaydettiğimiz veri nereye gitti.?

bunun için yine terminale gitmemiz lazım.

docker ps komutu, o an docker’ımızda çalışan container’ları göserir. Kırmızı kutu içerisinde gördüğümüz üzere bizim çalıştırdığımız Mongo container’ı 1905 portunda, latest versionu ile, kendisine ait bir unique id ile ve my-mongo-poc adıyla çalışmakta.

Bu unique id ya da container name ile şimdi MongoSH’a bağlanmamız lazım.

Bu yüzden yazmamız gereken terminal komutu

docker exec -it ab27 mongosh

Bu komut bizi docker’da çalışan mongo container’ın shell’ine gönderecek. ab27 aslında container id’sinin ilk 4 hanesi. Eğer bu ab27 değeri ile başlayan başka bir container yoksa (ki olması imkansız) direkt mongo container’ı çalışacaktır. Yani çalıştırmak istediğiniz container’ın ilk 4 hanesi ile container’ı çalıştırabilirsiniz. Tabi container id’si yerine direkt container name’i ile de sh’a gidebilirdik.

Artık, sh içerisindeyiz ve show dbskomutu ile container’ımız içerisindeki database’lere ve collectionlara ulaşabiliriz.

Burada şu anlık default olarak gelen admin ve bizim eklediğimiz blog-post-content db’si yer almakta. Ama 50 tane daha db olabilirdi. Hangi db’yi kullanmak istediğimizi

use blog-post-content

komutu ile shell’e yazmamız lazım.

Artık bizim oluşturuğumuz db’nin içerisindeyiz.

Fakat db’miz içerisinde oluşturduğumuz Writer ve Post adında 2 tane collection’umuz var.

Bunlara da

db.getCollectionNames() methodu ile erişebiliriz.

Artık bundan sonra mongoDB query’leri yazmaya başlayabiliriz.

Bir request atıp 200 sonucunu almıştık.

db.writer.find() komutu ile kaydettiğimiz veriyi görebiliriz.

ADIM 6

Şimdi bir kaç tane daha request atıp, bir kaç tane daha mongo query’si yazalım.

Yukarıdaki veriler üzerinden filtreleme yapabiliriz. Mesela userName’i tjc olan data’yı ekrana getirmek isteyelim.

Bunun için mongoDB query’lerini kullanabiliriz.

userName’i tjc olan veriyi find methoduna verdiğimizde bu veri bize gelecektir.

Yani demeye çalıştığım, burada terminal ekranında dilediğimiz gibi mongo komutları çalıştırabiliriz.

ADIM 7

Peki biz bu database’i bir görsel üzerinde göremez miyiz?

Bunun için olması gereken aslında bir tane mongoDB toolu. RoboMongo, Mongo Compass böyle bir iş için kullanılabilir. Ama DataGrip ya da Intellij IDE üzerinden de gerekli ayarlamalar yapıldığında veriler çok rahat bir şekilde görülebilir.

Intellij IDE ekranında en sağ taraftaki kırmızı yuvarlak içine alınan database simgesine tıkladıktan sonra, Mavi kutu içindeki sarı kutunun içindeki “+” butonuna basmamız lazım.

Açılan panelde en üstteki “Data Source” sekmesine gelip klavyeden “Mon” yazdığımızda MongoDB karşımıza gelir ve o sekmeye girmemiz lazım.

Yukarıdaki ekran açıldığında ise pembe kutudaki URL kısmını ayarladıktan sonra, sonrasında yeşil kutu içindeki Test Connection butonuna basmamız yeterli olacaktır. Success’i aldıktan sonra “Apply” ve “Ok” butonlarına tıkladığımızda artık bir konsol ekranı bizi karşılar.

Ekranın sağ tarafında mor kutunun içerisinde database’imizi görmemiz gerekmektedir. Eğer database’i görmezsek mor kutunun içerisindeki sarı yuvarlak içindeki “yenile” butonuna tıklayarak shell’de eklediğimiz verileri görmemiz yeterlidir.

NOT: Post collectionuna bir istek atılmadığı için henüz oluşmadı.

Konsol ekranına kahverengi kutudaki kodu yazıp çalıştırdığımız zaman, mavi kutudaki gibi sonucu görmüş oluruz ve görsel ile bu şekilde çalışmış oluruz.

DOCKER — REDIS — SPRING BOOT

ADIM 1

Redis için öncelikle bir tane docker commandi çalıştırmamız lazım ki bu da aslında mongodb docker komutuyla aynı diyebiliriz.

docker run -d -it -p 6379:6379  --name my-redis-poc redis:latest

ADIM 2

Redis için application.yml dosyamıza gerekli konfigürasyonlarımızı eklememiz gerekmekte.

#REDIS-CFG
redis:
host: my-redis-poc
port: 6379

application.yml dosyamızın son hali aşağıdaki gibi oldu.

#SERVER-CFG
server:
port: 8282

#MONGO-CFG
spring:
data:
mongodb:
host: localhost
port: 1905
database: blog-post-content

#REDIS-CFG
redis:
host: my-redis-poc
port: 6379

logging:
level:
org:
springframework:
data:
mongodb:
core:
MongoTemplate: DEBUG

Not: logging, tamamen optional.

ADIM 3:

Redis ile alakalı konfigürasyonları yapmamız gerekmektedir. Redis ile alakalı daha detaylı bilgi almak için ve core java’daki kullanımını detaylı bir şekilde okumak için buraya tıklayabilirsiniz.

Öncelikle docker’da ayağa kalkan redis’e erişmemiz lazım. Bunun için bazı konfigürasyonları yapmamız lazım.

@Configuration
public class RedisConfig {

@Value("${spring.redis.host}")
private String redisHost;

@Value("${spring.redis.port}")
private int redisPort;


@Bean
public RedisConnectionFactory redisConnectionFactory() {

RedisStandaloneConfiguration redisConfiguration =
new RedisStandaloneConfiguration();

redisConfiguration.setHostName(redisHost);
redisConfiguration.setPort(redisPort);
return new LettuceConnectionFactory(redisConfiguration);

}

}
  • redisConnectionFactory methodu, Redis'e bağlantı sağlayan bir RedisConnectionFactory nesnesi oluşturur. Bu bean, Redis ile etkileşimde bulunmak için Spring'in RedisTemplate veya başka Spring Data Redis araçlarını kullanırken gereklidir.
  • LettuceConnectionFactory kullanılarak yeni bir bağlantı factory’si oluşturulur. Lettuce, Redis için popüler bir Java client kütüphanesidir ve asenkron ve non-blocking Redis erişimi sağlar.
  • RedisStandaloneConfiguration sınıfı, Redis sunucusunun stand-alone modda çalıştığı durumlar için yapılandırmayı sağlar. Burada, Redis sunucusunun host adı ve portu, daha önce @Value anotasyonları ile enjekte edilen değerler kullanılarak ayarlanır.

ADIM 4:

Adım 4 ile birlikte, redis connection için gerekli konfigürasyonları yapmış olduk. Şimdi Cache ile alakalı konfigürasyonları yapmamız lazım.

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class CacheConfig {

private final RedisConnectionFactory redisConnectionFactory;

@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(10))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(
new GenericJackson2JsonRedisSerializer())
);
}

@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
.builder(redisConnectionFactory)
.withCacheConfiguration("PostCache", cacheConfiguration())
.withCacheConfiguration("WriterCache", cacheConfiguration());

return builder.build();
}

}

Buradaki kodları teker teker anlamak istediğimiz noktada;

  • @EnableCaching annotationu Spring’in cache destek özelliklerini etkinleştirir.
  • cacheConfiguration methodu, RedisCacheConfiguration türünde bir bean oluşturur. Bu konfig, Redis tabanlı cache için kullanılır.
  • RedisCacheConfiguration.defaultCacheConfig() statik metodu, Redis cache için varsayılan bir yapılandırma sağlar.
  • .entryTtl(Duration.ofSeconds(10)): Bu satır, cache'deki her bir kaydın (entry) ne kadar süreyle tutulacağını belirtir. Bu örnekte, her bir kayıt 10 saniye (dakika da olabilirdi) sonra süresi dolmuş sayılır ve cache'den kaldırılır. Bu, cache'in güncel kalmasını sağlar.
  • .disableCachingNullValues(): Bu satır, null değerlerin cache'lenmesini engeller. Yani, bir metod null bir değer döndürürse, bu değer cache'lenmez. Bu, cache'in gereksiz yere null değerlerle dolmasını önler ve daha verimli bir kullanım sağlar.
.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(
new GenericJackson2JsonRedisSerializer())
);
  • Bu kod parçası ise redis’teki değerleri serileştirilmiş bir şekilde görmemizi sağlayacak.

NOT: Burada başka konfigürasyonlar da kullanabiliriz. Örnek vermek gerekirse;

.prefixKeysWith("myApp:"): Cache anahtarlarına belirli bir prefix ekler. Bu, özellikle birden fazla uygulama veya cache grubu aynı Redis instance'ını kullanıyorsa, anahtar çakışmalarını önlemeye yardımcı olur.

.disableCaching(): Bu, caching'in tamamen devre dışı bırakılmasını sağlar. Test amaçlı veya belirli durumlarda kullanılabilir.

.publicCache(true/false): Cache'in herkese açık (public) veya özel (private) olup olmadığını belirler.

cacheManager() methodunda ise aslında hangi class’ları cachelemek istediğimizi ve hangi Redis konfigürasyonlarını kullanacağımızı belirtiyoruz.

ADIM 5:

Şimdi Post ile alakalı diğer Spring Boot class’larımızı hızlıca geliştirelim.

#POST-CONTROLLER
#---------------

@RequestMapping("/post")
@RestController
@RequiredArgsConstructor
public class PostController {

private final PostService postService;
private final Logger LOG = LoggerFactory.getLogger(getClass());


@GetMapping("/get-post-by-id/{id}")
public ResponseEntity<Post> getPostById(@PathVariable("id") String id) {
LOG.info("Getting Post with Id : {} ", id);
return ResponseEntity.ok().body(postService.getPostById(id));
}

@GetMapping("/get-all-posts")
public ResponseEntity<List<Post>> getAllPosts() {
LOG.info("Getting all posts");
return ResponseEntity.ok().body(postService.getAllPosts());
}


@PostMapping("/add-post")
public ResponseEntity<Post> addPost(@RequestBody Post post) {
LOG.info("Saving Post");
return ResponseEntity.ok().body(postService.addPost(post));
}


@GetMapping("/get-all-posts-by-writer-id/{id}")
public ResponseEntity<List<Post>> getAllPostsByWriterId(@PathVariable("id") String id) {
return ResponseEntity.ok().body(postService.getAllPostsByWriterId(id));
}


}
#POST-SERVICE
#---------------

@Service
@EnableCaching
@RequiredArgsConstructor
public class PostService {

private final PostRepository postRepository;
private final Logger LOGGER = LoggerFactory.getLogger(PostService.class);


@Cacheable(value = "PostCache", key = "#id", unless = "#result == null")
public Post getPostById(String id) {
return postRepository
.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Writer Not Found"));
}


@Cacheable(value = "PostCache", key = "'all'")
public List<Post> getAllPosts() {
List<Post> postList = postRepository.findAll();

if (!postList.isEmpty()) {
LOGGER.info("Bütün post belgeleri Redis önbelleğine eklendi.");
} else {
LOGGER.info("Post belgeleri bulunamadı.");
}

return postList;
}



public Post addPost(Post post) {
return postRepository.save(post);
}



public List<Post> getAllPostsByWriterId(String id) {
return postRepository.getPostByWriterId(id);
}

}
#POST-REPOSITORY
#---------------

public interface PostRepository extends MongoRepository<Post, String> {

@Query("{ 'writer._id' : ?0 }")
List<Post> getPostByWriterId(String writerId);

Optional<Post> getPostById(String postId);

}

Buradaki kodları açıklamak gerekirse, (Özellikle Service katmanını) açıklamak gerekirse,

@Cacheable(value = "PostCache", key = "#id", unless = "#result == null")
public Post getPostById(String id) {
return postRepository
.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Writer Not Found"));
}

Bu method aslında çok basit bir şekilde mongo’dan veri çekip, çektiği bu veriyi gösterir. Ama @Cachable annotationu içerisinde hangi Cache’in kullanılacağını söylüyoruz ve bunu id keyiyle yapmasını istediğimizi söylüyoruz.

@Cacheable(value = "PostCache", key = "'all'")
public List<Post> getAllPosts() {
List<Post> postList = postRepository.findAll();

if (!postList.isEmpty()) {
LOGGER.info("Bütün post belgeleri Redis önbelleğine eklendi.");
} else {
LOGGER.info("Post belgeleri bulunamadı.");
}

return postList;
}

Burada da ‘all’ keyi ile bütün verileri cache’lemesi gerektiğini söylüyoruz.

ADIM 6:

Şimdi localhost:8282/post/add-post requestlerimizi atarak Post modelimizi dolduralım.

   {
"id": "6573a0e6f5ebb0020d080e43",
"title": "Docker - MongoDB - Redis - Spring Boot",
"content": "Bir spring boot uygulamasi yaziyoruz.
Bu uygulamayi yaparken redis ve mongodb docker uzerinde
calisacak, ve spring boot docker uzerinde calisan bu toollara
baglanacak.",
"writer": {
"id": "6572538af2ae7a46222004d1"
}
}
    {
"id": "6573a183f5ebb0020d080e44",
"title": "Isık Hızında Bir Yolculuk; Internet",
"content": "Hic dusundunuz mu, internet nasil calisir.?
Her gun kullandigimiz interneti ama gercekte acaba
interneti ne kadar taniyoruz.?",
"writer": {
"id": "6572538af2ae7a46222004d1"
}
}
    {
"id": "6573a257f5ebb0020d080e45",
"title": "Functional Programming Nedir.?",
"content": "Turkiye Java Community'nin bu ayki Ankara TekMer
etkinliginde co-founderimiz Furkan Sahin Kulaksiz
functional Programming uzerine sunum yapacak",
"writer": {
"id": "657265e0f2ae7a46222004d3"
}
}
    {
"id": "6573a28bf5ebb0020d080e46",
"title": "12 Factor App",
"content": "Turkiye Java Community'nin bu ayki Ankara TekMer
etkinliginde co-founderimiz Evren TAN yazilim muhendisliginin
en onemli konularindan birisini anlatacak",
"writer": {
"id": "657265e0f2ae7a46222004d3"
}
}
    {
"id": "6573a2caf5ebb0020d080e47",
"title": "Clean Code",
"content": "Bu kitabi lutfen okuyalim",
"writer": {
"id": "657265b2f2ae7a46222004d2"
}
}
    {
"id": "6573a311f5ebb0020d080e48",
"title": "FolksDev is everything",
"content": "Cagri Dursun'un kurdugu FolksDev, her hafta persembe
youtube'da yayin yapar. Kacirmayin.",
"writer": {
"id": "65726610f2ae7a46222004d4"
}
}

Bu adımla birlikte hem Post hem de Writer modellerimizi doldurmuş olduk.

ADIM 7:

Şimdi @Cachable annotationu kullandığımız service methodlarına bir istek atalım.
Örn; localhost:8282/post/get-post-by-id/6573a0e6f5ebb0020d080e43

Redis’de bu değeri görmek için ise şimdi docker’a gitmemiz gerkmekte.

docker exec -it my-redis-poc redis-cli

komutunu terminale yazdığımızda artık redis-cli ekranındayız ve redis komutlarımızı yazmaya başlayabiliriz.

KEYS * komutu ile PostCache’imize id değeri ile eklediğimiz key değerini gördük. Peki içeriğini nasıl görebiliriz.?

GET KEY ile içeriğimizi de görebiliriz.

Bu komutlarla alakalı çok daha detaylı bilgi almak için buraya tıklayabilirsiniz.

Peki aradan 20 sn geçtikten sonra redis-cli’da tekrardan KEYS * komutunu çalıştırırsak ne olur.?

Empty Array değerini gördük çünkü Redis konfigürasyonumuzu yaparken TTL değerimizi 20 sn verdik.

Bir de get-all-post endpointine bir istek atalım.

Aslında burada da bütün değerleri all key’i ile çektiğimiz gördük.

NOT: redis konfigürasyonumuzu tanımlarken
.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(
new GenericJackson2JsonRedisSerializer()));

şeklinde bir kod yazmıştık. Eğer bunu yazmasaydık, çıktımız aşağıdaki gibi olacaktı.

ADIM 8:

Peki redis’i görselde göremez miyiz.?
Intellij IDE’de ya da DataGript’te görsel olarak redis’i görebiliriz. Tabii başka çözümler de muhakkak vardır ama ben Intellij IDE üzerinden bunu yapacağım.

Aslında bu adımlar tamamen mongoDB’deki görselleştirmeler ile aynı.

  1. IDE’nin sağ tarafında bulunan Database iconuna tıkla.
  2. Açılan Database tabında sol üstteki artı butonuna tıkla.
  3. 2. sıradaki DataSource tabına git ve redis’i seç. (redis yazarak çok kolay bir şekilde ulaşabilirsin.)
  4. Açılan ekranda URL kısmına jdbc:redis://localhost:6379/0 yerine jdbc:redis://localhost:1905/0 yaz.
  5. Aşağıdaki test connection butonuna tıkla.
  6. Success aldıktan sonra Apply ve Ok butonları ile query konsoluna geçiş yap.
  7. Optional -> Bu tab’daki en üstteki name kısmına redis konsoluna isim verebilirsin.

Tam burada aslında daha kolay işlem yapabilmek için konfigürasyondaki TTL kısmını değiştirip 20 dk yapacağım ve requestleri postman üzerinden tekrar atacağım.

Konsol ekranına siyah kutudaki KEYS * sorgusunu yazdığım zaman, kırmızı kutudaki değerleri gördüm.

Get komutunu çalıştırdığım zaman ise,

Görseldeki gibi sorgumu güzel bir şekilde output ekranında gördüm.

Okuduğunuz için teşekkür ederim.

Projenin kaynak kodlarına buradan ulaşabilirsiniz.

Projenin kaynak kodlarına buradan ulaşabilirsiniz.

İletişim, İşbirlikleri ve Teklifler için,

github: https://github.com/fsk

bitbucket: https://bitbucket.org/furkandev

twitter: https://twitter.com/0xfsk

mail: furkansahinkulaksiz@gmail.com

linkedIn: https://www.linkedin.com/in/frknshnklksz/

superpeer: https://superpeer.com/fsk

--

--