MongoDB ile Bir GO Uygulamasını Konuşturmak

Teknoloji baş döndüren bir hızla ilerlerken beynimizin tembelleştiğini de kabul etmemiz gerekiyor. Artık pek çok işimiz otonomlaştırıldığından zihnimiz eski egzersizleri yapmıyor. Yıllar önce İngiltere'de yapılan bir araştırmada çocukların hesap makinesi kullanması sebebiyle temel dört işlem matematiğinde sorunlar yaşadığı tespit edilmişti. Yine Kanada'da yapılan bir araştırma insanların dikkat dağılma sürelerinin 8 saniyelere kadar indiğini gösterdi. Hafızamızı dinç tutma noktasında Japon balıkları ile yarışır bir konumda olduğumuz da aşikar. Kaçımız aklından ezbere 4 telefon numarasını sayabilir(Üç haneliler yasak) Otonomlaşan dünya sebebiyle tembelleşen ve dış uyarıcılar yüzünden sürekli dikkati dağılan zihnimiz...Gerçekten de dikkatimizi dağıtan, odaklanmamızı bozan o kadar çok şey var ki. Dolayısıyla kendimizi yetiştirmek istediğimiz konulara çalışırken ne kadar verimli olabiliyoruz bir bakmak gerekiyor. Tekrar satın alınamayacak olan zamanın ne kadar kıymetli olduğunu düşünürsek verimli çalışmanın ilerleyen yaşlarda çok çok önemli bir mesele haline geldiğini vurgulamak isterim.
 
İşte bu sebepten birkaç haftadır Saturday-Night-Works çalışmalarım sırasında pomodoro tekniğini neden kullanmadığımı karar kara düşünmekteyim. Oysa ki çok verimli bir çalışma pratiği. Atladığım bu önemli detayı SkyNet çalışmalarımın ilk gününden itibaren uygulamaya karar verdim. Genellikle gece 22:00 sularında başlayarak 4X25 dakikalık seanslar halinde ilerliyorum. Her seans arasında 5er dakikalık standart molalar var. Tabii bu tekniği uygularken en önemli kural çalışmayı bölecek unsurları mutlak suretle dışarıda bırakmak. Cep/ev telefonu, televizyon, radyo, e-mail programı ve benzeri odak dağıtıcı ne kadar şey varsa kapatmak gerekiyor. Bunun faydasını epeyce gördüğümü ve 25 dakikalık zaman dilimlerindeki çalışmalardan iyi seviyede verim aldığımı ifade edebilirim. Aranızda uygulamayanlar varsa bir göz atsınlar derim ;) 
 
Pomodoro tekniğini uygularken size akıllı bir kronometre gerekecek. Tarayıcıda çalışan Tomato-Timer tam size göre. Hatta Visual Studio Code için eklentisi bile var ;)
Gelelim SkyNet'te geçirdiğim ikinci güne. Elimizdeki malzemeleri sayalım. MongoDB için bir docker imajı, gRPC ve GoLang. Bu üçünü kullanarak CRUD(Create Read Update Delete) operasyonlarını icra eden basit bir uygulama geliştirmek niyetindeyim. Bir önceki öğretide Redis docker container'dan yararlanmıştım. Kaynakları kıymetli olan Ahch-To sistemini kirletmemek adına MongoDB için de benzer şekilde hareket edeceğim. Açıkçası GoLang bilgim epey paslanmış durumda ve sistemde yüklü olup olmadığını dahi bilmiyorum.
go version
terminal komutu da bana yüklü olmadığını söylüyor. Dolayısıyla ilk adım onu MacOS üzerine kurmak.

İlk Hazırlıklar(Go Kurulumu ve MongoDB)

GoLang'i Ahch-To adasına yüklemek için şu adrese gidip Apple macOS sürümünü indirmem gerekti. Ben öğretiyi hazırlarken go1.13.4.darwin-amd64.pkg dosyasını kullandım. Kurulum işlemini tamamladıktan sonra komut satırından go versiyonunu sorgulattım ve aşağıdaki çıktıyı elde ettim.
 
 
Pek tabii içim rahat değildi. Versiyon bilgisi gelmişti ama bir "hello world" uygulamasını çalışır halde görmeliydim ki kurulumun sorunsuz olduğundan emin olayım. Hemen resmi dokümanı takip ederek $HOME\go\src\ altında helloworld isimli bir klasör açıp aşağıdaki kod parçasını içeren helloworld.go dosyasını oluşturdum(Visual Studio Code kullandığım için editörün önerdiği go ile ilgili extension'ları yüklemeyi de ihmal etmedim)
package main

import "fmt"

func main() {
	fmt.Printf("Artık go çalışmaya hazırım :) \n")
}
Terminalden aşağıdaki komutları işlettikten sonra çıktıyı görebildim. 
go build
./helloworld
Go ile kod yazabildiğime göre MongoDB docker imajını indirip bir deneme turuna çıkabilirim. İşte terminal komutları.
docker pull mongo
docker run -d -p 27017-27019:27017-27019 --name gondor mongo
docker container ps -a
docker exec -it gondor bash

mongo
show dbs
use AdventureWorks
db.category.save({title:"Book"})
db.category.save({title:"Movie"})
db.category.find().pretty()

exit
exit
 
İlk komutla mongo imajı çekiliyor. İzleyen komut docker container'��nı varsayılan portları ile sistemin kullanımına açmak için. Container listesinde göründüğüne göre sorun yok. MongoDB veritabanını container üzerinden test etmek amacıyla içine girmek lazım. 4ncü komutu bu işe yarıyor. Ardından mongo shell'e geçip bir kaç işlem gerçekleştirilebilir. Önce var olan veritabanlarını listeliyor sonra AdventureWorks isimli yeni bir tane oluşturuyoruz. Devam eden kısımda category isimli bir koleksiyona iki doküman ekleniyor ve tümünü güzel bir formatta listeliyoruz. Arka arkaya gelen iki exit komutunu fark etmişsinizdir. İlki mongo shell'den, ikincisi de container içinden çıkmak için.

Ah çok önemli bir detayı unuttum! Örnekte gRPC protokolünü kullanacağız. Bu da bir proto dosyamız olacağı ve Golang için gerekli stub içeriğine derleyeceğimiz anlamına geliyor. Dolayısıyla sistemde protobuf ve go için gerekli derleyici eklentisine ihtiyacım var. brew ile bunları sisteme yüklemek oldukça kolay.
brew install protobuf
protoc --version
brew install protoc-gen-go
Kod tarafına geçmeye hazırız ama öncesinde ufak bir bilgi.

gRPC Hakkında Azıcık Bilgi

gRPC, HTTP2 bazlı modern bir iletişim protokolü ve JSON yerine ProtoBuffers olarak isimlendirilen kuvvetle türlendirilmiş bir ikili veri formatını kullanmakta(strongly-typed binary data format) JSON özellikle REST tabanlı servislerde popüler bir format olmasına rağmen serileştirme sırasında CPU'yu yoran bir performans sergiliyor. HTTP/2 özelliklerini iyi kullanan gRPC ise 5 ile 25 kata kadar daha hızlı. Bu noktada hatırlamak için bile olsa gRPC ile REST'i kıyaslamakta yarar var. İşte karşılaştırma tablosu.
 
 REST Tarafı gRPC Tarafı 
 HTTP 1.1 nedeniyle gecikme yüksek    HTTP/2 sebebiyle daha düşük gecikme 
 Sadece Request/Response  Stream desteği(Örneğimizde bir kullanımı var)
 CRUD odaklı servisler için   API odaklı(Burada CRUD odaklı yapacağız çaktırmayın)
 HTTP Get,Post,Put,Delete gibi fiil tabanlı  RPC tabanlı, sunucu üzerinden fonksiyon çağırabilme özelliği
 Sadece Client->Server yönlü talepler  Çift yönlü ve asenkron iletişim
 JSON kullanıyor(serileşme yavaş, boyut büyük)  Protobuffer kullanıyor(veri daha küçük boyutta ve serileşme hızlı)

Örnek Uygulama

Gelelim kod tarafına... Uygulamanın temel klasör yapısını aşağıdaki gibi oluşturabiliriz. Ben bu işlemleri $HOME\go\src\ altında gerçekleştirdim.

mkdir gRPC-sample
cd gRPC-sample
mkdir playerserver
mkdir clientapp
mkdir proto
playerserver ve clientapp tahmin edileceği üzere sunucu ve istemci uygulama görevini üstleniyorlar. proto klasöründe yer alan player.proto, gRPC mesaj sözleşmesine ait tanımlamaları içermekte. Servis metodları, parametre tipleri ve içerikleri bu dosyada aşağıdaki gibi bildiriliyor. 
syntax="proto3"; //protobuffers v3 versiyonu kullaniliyor

package player; // proto paketinin adi
option go_package="playerpb"; // generate edilecek go paketinin adi

// Player mesaj tipinin tanimi
message Player{
    string id=1;
    string player_id=2;
    string fullname=3;
    string position=4;
    string bio=5;
}

// Operasyonlarin kullandigi request ve response mesajlarina ait tanimlamalar
message AddPlayerReq{
    Player plyr=1;
}

message AddPlayerRes{
    Player plyr=1;
}

message EditPlayerReq{
    Player plyr=1;
}

message EditPlayerRes{
    Player plyr=1;
}

message RemovePlayerReq{
    string player_id=1;
}

message RemovePlayerRes{
    bool removed=1;
}

message GetPlayerReq{
    string player_id=1;
}

message GetPlayerRes{
    Player plyr=1;
}

message GetPlayerListReq{}

message GetPlayerListRes{
    Player plyr=1;
}

// servis ve operasyon tanimlari
service PlayerService{
    rpc GetPlayer(GetPlayerReq) returns (GetPlayerRes);
    rpc GetPlayerList(GetPlayerListReq) returns (stream GetPlayerListRes); //server bazlı streaming kullanacağımız için dönüş parametresi stream tipinden
    rpc AddPlayer(AddPlayerReq) returns (AddPlayerRes);
    rpc EditPlayer(EditPlayerReq) returns (EditPlayerRes);
    rpc RemovePlayer(RemovePlayerReq) returns (RemovePlayerRes);
}
Bu içeriği Go tarafında kullanabilmek için derlememiz lazım. Derlemeyi aşağıdaki terminal komutu ile gerçekleştirebiliriz(proto dosyasını VS Code tarafında daha kolay düzenlemek için vscode-proto3 isimli extension'ı kullandım)
protoc player.proto --go_out=plugins=grpc:.
Proto dosyasının tamamlanmasını takiben playerserver klasöründeki main.go dosyasını yazmaya başlayabiliriz. Biraz uzun bir kod dosyası ama sabırla yazıp, yorum satırlarını da okuyarak neler yaptığımızı anlamaya çalışmakta yarar var.
package main

import (
	"context"
	"fmt"
	"net"
	"os"
	"os/signal"
	"strings"

	"go.mongodb.org/mongo-driver/bson/primitive"

	playerpb "gRPC-sample/proto"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

/* proto'dan otomatik üretilen player.pb.go içerisindeki RegisterPlayerServiceServer metoduna bir bakın.
Pointer olarak gelen grpc server nesnesi ikinci parametre olarak gelen tipi register etmek için kullanılır.
Bir nevi interface üzerinden enjekte işlemi yaptığımızı düşünebilir miyiz?
*/
type PlayerServiceServer struct{}

var db *mongo.Client
var playerCollection *mongo.Collection
var mongoContext context.Context

func main() {
	// TCP üzerinden 5555 nolu portu dinleyecek olan nesne oluşturuluyor
	server, err := net.Listen("tcp", ":5555")
	// Olası bir hata durumunu kontrol ediyoruz
	if err != nil {
		fmt.Printf("5555 dinlenemiyor: %v", err)
	}

	// gPRC sunucusu için kayıt(register) işlemleri
	grpcOptions := []grpc.ServerOption{}
	// yeni bir grpc server oluşturulur
	grpcServer := grpc.NewServer(grpcOptions...)
	// Bir PlayerService tipi oluşturulur
	playerServiceType := &PlayerServiceServer{}
	// servis sunucu ile birlikte kayıt edilir
	playerpb.RegisterPlayerServiceServer(grpcServer, playerServiceType)

	// mongoDB bağlantı işlemleri
	fmt.Println("MongoDB sunucusuna bağlanılıyor")
	mongoContext = context.Background()
	// bağlantı deneniyor
	db, err = mongo.Connect(mongoContext, options.Client().ApplyURI("mongodb://localhost:27017"))
	// olası bir bağlantı hatası varsa
	if err != nil {
		fmt.Println(err)
	}
	// Klasik ping metodunu çağırıyoruz
	err = db.Ping(mongoContext, nil)
	if err != nil {
		fmt.Println(err)
	} else {
		// çağrı başarılı olursa bağlandık demektir
		fmt.Println("MongoDB ile bağlantı sağlandı")
	}
	// nba isimli veritabanındaki player koleksiyonuna ait bir nesne örnekliyoruz
	// veritabanı ve koleksiyon yoksa oluşturulacaktır
	playerCollection = db.Database("nba").Collection("player")

	// gRPC sunucusunu aktif olan TCP sunucusu içerisinde bir child routine olarak başlatıyoruz
	go func() {
		if err := grpcServer.Serve(server); err != nil {
			fmt.Println(err)
		}
	}()
	fmt.Println("Sunucu 5555 nolu porttan gPRC tabanlı iletişime hazır.\nDurdurmak için CTRL+C.")

	// CTRL+C ile başlayan kapatma operasyonu
	cnl := make(chan os.Signal)      // işletim sisteminde sinyal alabilmek için bir kanal oluşturduk
	signal.Notify(cnl, os.Interrupt) // CTRL+C mesajı gelene kadar ana rutin açık kalacak
	<-cnl

	fmt.Println("Sunucu kapatılıyor...")
	grpcServer.Stop() // gRPC sunucusunu durdur
	server.Close()    // TCP dinleyicisini kapat
	fmt.Println("GoodBye Crow")
}

/* Protobuf mesajlarında taşınan serileşmiş içeriği nesnel olarak ele alacağımı struct */
type Player struct {
	ID       primitive.ObjectID `bson:"_id,omitempty"` // MongoDB tarafındaki ObjectId değerini taşır
	PlayerID string             `bson:"player_id"`
	Fullname string             `bson:"fullname"`
	Position string             `bson:"position"`
	Bio      string             `bson:"bio"`
}

/* PlayerServiceServer'ın uygulanması gereken metodlarını. Yani servis sözleşmesinin tüm operasyonları
 */

// Yeni bir oyuncu eklemek için kullanacağımız fonksiyon
func (srv *PlayerServiceServer) AddPlayer(ctx context.Context, req *playerpb.AddPlayerReq) (*playerpb.AddPlayerRes, error) {
	payload := req.GetPlyr() // GetPlyr (GetPlayer değil o servis metodumuz) fonksiyonu ile request üzerinden gelen player içeriği çekilir

	// İçerik ile gelen alan değerleri player struct nesnesini oluşturmak için kullanılır
	player := Player{
		PlayerID: payload.GetPlayerId(),
		Fullname: payload.GetFullname(),
		Position: payload.GetPosition(),
		Bio:      payload.GetBio(),
	}

	// player nesnesi mongodb veritabanındaki koleksiyona kayıt edilir
	result, err := playerCollection.InsertOne(mongoContext, player)

	// bir problem oluştuysa
	if err != nil {
		// gRPC hatası döndürülür
		return nil, status.Errorf(
			codes.Internal,
			fmt.Sprintf("Bir hata oluştu : %v", err),
		)
	}

	// Hata oluşmadıysa koleksiyona eklenen yeni doküman
	// üretilen ObjectID değeri de atanarak geri döndürülür
	objectID := result.InsertedID.(primitive.ObjectID)
	payload.Id = objectID.Hex()
	return &playerpb.AddPlayerRes{Plyr: payload}, nil
}

func (srv *PlayerServiceServer) EditPlayer(ctx context.Context, req *playerpb.EditPlayerReq) (*playerpb.EditPlayerRes, error) {
	return nil, nil
}

func (srv *PlayerServiceServer) RemovePlayer(ctx context.Context, req *playerpb.RemovePlayerReq) (*playerpb.RemovePlayerRes, error) {
	// önce silinmek istenen playerId bilgisi alınır
	id := strings.Trim(req.GetPlayerId(), "\t \n")
	fmt.Println(id)
	// DeleteOne metodu ile silme operasyonu gerçekleştirilir
	_, err := playerCollection.DeleteOne(ctx, bson.M{"player_id": id})

	// hata kontrolü yapılıyor
	if err != nil {
		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Silinmek istenen oyuncu bulunamadı. %s", err))
	}

	// hata yoksa işlemin başarılı olduğuna dair sonuç dönülür
	return &playerpb.RemovePlayerRes{
		Removed: true,
	}, nil
}

// MongoDB'deki ID bazlı olarak oyuncu verisi döndüren metodumuz
func (srv *PlayerServiceServer) GetPlayer(ctx context.Context, req *playerpb.GetPlayerReq) (*playerpb.GetPlayerRes, error) {
	// request ile gelen player_id bilgisini alıyoruz
	// Trim işlemi önemli. İstemci terminalden değer girdiğinde alt satıra geçme işlemi söz konusu.
	// Veri bu şekilde gelirse kayıt bulunamaz. Dolayısıyla bir Trim işlemi yapıyoruz
	id := strings.Trim(req.GetPlayerId(), "\t \n")
	// bson.M metoduna ilgili sorguyu ekleyerek oyuncuyu koleksiyonda arıyoruz
	result := playerCollection.FindOne(ctx, bson.M{"player_id": id})

	player := Player{}
	// bulunan oyuncu decode metodu ile ters serileştirilip player değişkenine alınır
	if err := result.Decode(&player); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("Sanırım aranan oyuncu bulunamadı %v", err))
	}

	// Decode işlemi başarılı olur ve koleksiyondan bulunan içerik player isimli değişkene ters serileşebilirse
	// artık dönecek response nesne içeriğini hazırlayabiliriz
	res := &playerpb.GetPlayerRes{
		Plyr: &playerpb.Player{
			Id:       player.ID.Hex(),
			PlayerId: player.PlayerID,
			Fullname: player.Fullname,
			Position: player.Position,
			Bio:      player.Bio,
		},
	}
	return res, nil
}

// Tüm oyuncu listesini stream olarak dönen metod
func (srv *PlayerServiceServer) GetPlayerList(req *playerpb.GetPlayerListReq, stream playerpb.PlayerService_GetPlayerListServer) error {

	currentPlayer := &Player{}

	// Find metodu veri üzerinden hareket edebileceğimiz bir Cursor nesnesi döndürür
	// bu cursor nesnesi sayesinde istemciye tüm oyuncu listesini bir seferde göndermek yerine
	// birer birer gönderme şansına sahip olacağız
	// Bu nedenle sunucu bazlı bir streamin stratejimiz var
	cursor, err := playerCollection.Find(context.Background(), bson.M{})
	if err != nil {
		return status.Errorf(codes.Internal, fmt.Sprint("Bilinmeyen hata oluştu"))
	}

	// metod işleyişini tamamladığında cursor nesnesini kapatacak çağrıyı tanımlıyoruz
	defer cursor.Close(context.Background())

	// iterasyona başlanır ve Next true döndüğü sürece devam eder
	// yani okunacak mongodb dokümana kalmayana dek
	for cursor.Next(context.Background()) {
		// cursor verisini currentPlayer nesnesine açıyoruz
		cursor.Decode(currentPlayer)
		// istemciye mongodb'den gelen güncel oyuncu bilgisinden yararlanarak cevap dönüyoruz
		stream.Send(&playerpb.GetPlayerListRes{
			Plyr: &playerpb.Player{
				Id:       currentPlayer.ID.Hex(),
				PlayerId: currentPlayer.PlayerID,
				Fullname: currentPlayer.Fullname,
				Position: currentPlayer.Position,
				Bio:      currentPlayer.Bio,
			},
		})
	}

	return nil
}
Sunucu tarafındaki kodlama tamamlandıktan sonra istemci tarafı için clientapp altında tester.go isimli bir başka dosya oluşturarak ilerleyelim. Burada komut satırından temel CRUD operasyonlarını icra edeceğiz. Yeni bir oyuncunun eklenmesi, bir oyuncu bilgisinin çekilmesi, tüm oyuncuların listesinin alınması vb
package main

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"strings"

	playerpb "gRPC-sample/proto"

	"google.golang.org/grpc"
)

var client playerpb.PlayerServiceClient
var reqOptions grpc.DialOption

func main() {

	// HTTPS ayarları ile uğraşmak istemedim
	reqOptions = grpc.WithInsecure()
	// gRPC servisi ile el sıkışmaya çalışıyoruz
	connection, err := grpc.Dial("localhost:5555", reqOptions)
	if err != nil {
		fmt.Println(err)
		return
	}
	// proxy nesnesini ilgili bağlantıyı kullanacak şekilde örnekliyoruz
	client = playerpb.NewPlayerServiceClient(connection)

	// Oyuncu ekleyelim
	insertPlayer()
	// tüm oyuncu listesini çekelim
	getAllPlayerList()

	// sembolik olarak ID bazlı 3 oyuncu aratalım
	for i := 0; i < 3; i++ {
		reader := bufio.NewReader(os.Stdin)
		fmt.Println("Oyuncu IDsini gir.")
		playerID, _ := reader.ReadString('\n')
		getByPlayerID(playerID)
	}

	// Silme operasyonunu deniyoruz
	reader := bufio.NewReader(os.Stdin)
	fmt.Println("Silmek istediğiniz oyuncunun IDsini girin.")
	playerID, _ := reader.ReadString('\n')
	removePlayerByID(playerID)

	// tüm oyuncu listesini çekelim
	getAllPlayerList()
}

func insertPlayer() {
	// Yeni oyuncu eklenmesi için deneme kodu
	// Veri ihlalleri örneğin basitliği açısından göz ardı edilmiştir
	reader := bufio.NewReader(os.Stdin)
	fmt.Println("Yeni oyuncu girişi")
	fmt.Println("Id->")
	id, _ := reader.ReadString('\n')
	id = strings.Replace(id, "\n", "", -1)
	fmt.Println("Adı->")
	fullname, _ := reader.ReadString('\n')
	fullname = strings.Replace(fullname, "\n", "", -1)
	fmt.Println("Pozisyon->")
	position, _ := reader.ReadString('\n')
	position = strings.Replace(position, "\n", "", -1)
	fmt.Println("Kısa biografisi->")
	bio, _ := reader.ReadString('\n')
	bio = strings.Replace(bio, "\n", "", -1)

	// protobuf dosyasındaki şemayı kullanarak örnek bir oyuncu nesnesi örnekliyoruz
	newPlayer := &playerpb.Player{
		PlayerId: id,
		Fullname: fullname,
		Position: position,
		Bio:      bio,
	}

	// servisin AddPlayer metodunu o anki context üzerinden çalıştırıp
	// request payload içerisinde yeni oluşturduğumuz nesneyi gönderiyoruz
	res, err := client.AddPlayer(
		context.TODO(),
		&playerpb.AddPlayerReq{
			Plyr: newPlayer,
		},
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	// Eğer bir hata oluşmamışsa MongoDB tarafından üretilen ID değerini ekranda görmemiz lazım
	fmt.Printf("%s ile yeni oyuncu eklendi \n", res.Plyr.Id)
}

// Tüm oyuncu listesini çektiğimiz metod
func getAllPlayerList() {

	// önce request oluşturulur
	req := &playerpb.GetPlayerListReq{}

	// proxy nesnesi üzerinden servis metodu çağrılır
	s, err := client.GetPlayerList(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}

	// sunucu tarafından stream bazlı dönüş söz konusu
	// yani kaç tane oyuncu varsa herbirisi için sunucudan istemciye
	// cevap dönecek
	for {
		res, err := s.Recv() // Recv metodu player.pb.go içerisine otomatik üretilmiştir. İnceleyin ;)
		if err != io.EOF {   // döngü sonlanmadığı sürece gelen cevaptaki oyuncu bilgisini ekrana yazdırır
			fmt.Printf("[%s] %s - %s \n\n", res.Plyr.PlayerId, res.Plyr.Fullname, res.Plyr.Bio)
		} else {
			break
		}
	}
}

// Oyuncuyu PlayerID değerinden bulan metodumuz
func getByPlayerID(playerID string) {
	// parametre olarak gelen playerID değerinden bir request oluşturulur
	req := &playerpb.GetPlayerReq{
		PlayerId: playerID,
	}
	// GetPlayer servis metoduna talep gönderilir
	res, err := client.GetPlayer(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res.Plyr.Fullname)
}

// Oyuncu silme fonksiyonumuz
func removePlayerByID(playerID string) {
	// RemovePlayer servis çağrısı için gerekli Request tipi hazırlanır
	req := &playerpb.RemovePlayerReq{
		PlayerId: playerID,
	}
	// servisi çağrısı yapılıp sonucu kontrol edilir
	_, err := client.RemovePlayer(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Oyuncu silindi")
}
Piuvvv!!! Uzun bir yol oldu. Öyleyse çalışma zamanı sonuçlarımıza bakalım mı?

Çalışma Zamanı

İlk gün çalışmasının meyveleri pek fena değil. server ve client tarafa ait go dosyalarını kendi klasörlerinde aşağıdaki terminal komutları ile derledikten sonra
go build main.go
go build tester.go
önce sunucu ardından istemci programlarını çalıştırıp kodlaması ilk önce biten AddPlayer fonksiyonunu deneme şansı buldum. Birkaç oyuncu verisini girdikten sonra mongodb container'ına ait shell'e bağlanıp gerçekten de yeni dokümanların player koleksiyonuna eklenip eklenmediğine baktım. Sonuç tebessüm ettiriciydi :) İstemci uygulama gRPC üzerinden sunucuya mesaj göndermiş, sunucuya gelen içerik docker container üzerinde duran mongodb veritabanına yazılmıştı.
 
 
İkinci gün tüm oyuncu listesini gRPC üzerinden istemciye döndüren süreci yazmaya çalıştım. İlk başta yaptığım bir hata nedeniyle epey vakit kaybettim. GetPlayerList metodunu protobuffer dosyasında stream döndürecek şekilde tasarlamamıştım. Büyük bir veri kümesini filtresiz çektiğimizde bu ağ trafiğinin sağlıklı çalışması açısından sorun olabilir. Oyuncuları sunucudan istemciye doğru bir stream üzerinden tek tek göndermek çok daha mantıklı(Burada REST ile gRPC arasındaki farkları hatırlayalım) Sonunda servis sözleşmesini değiştirip gerekli düzenlemeleri yaptıktan sonra aşağıdaki ekran görüntüsünde yer alan mutlu sona ulaşmayı başardım.
 
 
Devam eden gün bir öncekine göre daha zorlu geçti. FindOne metodunu player_id değerine göre çalıştırmayı bir türlü başaramadım. Neredeyse 4 pomodoro periyodu uğraştım. Hatta pomodoro süreci bittikten sonra farkında olmadan saatlerce bilgisayar başında kaldım. Sorunu araştırırken vakit nasıl geçti anlamamışım. Sonuçta işe 3 saatlik uykuyla gittim. Ertesi gün Ahch-To'nun tuşuna bile basmadım. Bir günlük ara, problemi çözmem için beni sakinleştirmeye yeterdi. Nihayetinde sorunu buldum. İstemci aradığı ID değerini girip sunucuya çağrı yaptığında, servis metoduna gelen ID bilgisinin sonunda boşluk ve alt satıra geçme karakterleri de geliyordu. Trim fonksiyonu ile bu durumun oluşmasını engelledikten sonra silme operasyonunu da işin içerisine dahil ettim ve güncelleme operasyonu hariç komple bir test yaptım. Sonuçlar ekran görüntüsünde olduğu gibi tatmin ediciydi.
 
Silme operasyonuna ilişkin çalışmaya ait örnek bir ekran görüntüsü de aşağıdaki gibi.

Neler Öğrendim?

Elbette SkyNet'te geçirdiğim bugünün de bana öğrettiği bir sürü şey oldu. Bunları aşağıda yer alan maddelerle ifade etmeye çalıştım.
  • Bir protobuf dosyası nasıl hazırlanır ve Go tarafında kullanılabilmesi için nasıl derlenir,
  • Go tarafından MongoDB ile nasıl haberleşilir,
  • MongoDB docker container'ına ait shell üstünde nasıl çalışılır,
  • Temel mongodb komutları nelerdir,
  • Sunucudan istemciye stream açarak tek tek mongo db dokümanı nasıl döndürülür(main.go'daki GetPlayerList metoduna bakın)

Eksikliği Hissedilen Konular

Her ne kadar pomodoro tekniği ile çalışmalarımı olabildiğince verimli hale getirsem de ister istemez yaşlı zihnim yoruluyor. Dolayısıyla şunları da yapabilsem iyi olurdu dediğim şeyler var. Bunları da şu iki madde ile sıralayabilirim.

  • İstemci tarafını Go tabanlı bir web client olarak geliştirmeyi deneyebiliriz. Terminalden hallice daha iyidir. En azından çalışma sırasında yaşadığım Trim ihlali oluşmaz.
  • Bir çok sunucu metodunda hata kontrolü var ancak bunların çalışıp çalışmadığı test etmek gerekiyor. Yani Code Coverage değerimizi neredeyse 0. Yazıyla sıfır :) Bir Go uygulamasındaki fonksiyonlar için Unit Test'ler nasıl yazılır öğrenmem lazım.

Görev Listeniz

Ve tabii kabul ederseniz sizin için iki güzel görevim var :)

  • Select * from players where fullname like 'A%' gibi bir sorguya karşılık gelecek mongodb fonksiyonunu geliştirip uygulamaya ekleyin.
  • Güncelleme fonksiyonunu tamamlayın.

Böylece geldik SkyNet'te bir günün daha sonuna. Sonraki çalışmada Wails paketini kullanarak Go ile yazılmış bir masaüstü programı geliştirmek niyetindeyim. O zaman dek hepinize mutlu günler dilerim.

Yorumlar (2) -

  • Merhaba hocam belki alakasız bir yorum ama internette araştırıp bulamadım cevabını sizin yardımcı olabileceğinizi düşündüm.

    Mssql de kullandıgım bu sorguyu mongoda nasıl kullanabilirim.
    “Update Doctors Set Talent=Talent.Replace(” “,”&“).ToLower()”
  • Merhaba, çok başarılı bir yazı olmuş, hani çok okumayı ya da kodları görene kadar okumayı sevmem ama yazınızı(GO hiç bilmeme rağmen) satır satır okudum, kod ve yorumlar dahil Smile Samimi, yalın, öz eleştirilerine bayıldığım, hiç duymadığım "böyle şeylerde varmış" dediğim bir yazı olmuş, bu ara da pomodoro nedir diye okudum yazında görünce videolar izledim( düşün yani kısa sürmedi yazının okunması takdir edin beeni Smile )  ilk fırsatta deneyeceğim. tebrik ediyorum yazı için. devamını beklerim, bende ufaktan bi go'ya bakayım..

    Not: TotalesX in sorusuna şu uygun olur mu bilmiyorum ;

    var collection = db.GetCollection("Foo");
    var query = Query.GTE("No", 1);
    var update = Update.Set("timestamp", datetime.UtcNow);
    collection.Update(query, update, UpdateFlags.Multi);

    gibi bir örnek belki iş görecektir.

Yorum ekle

Loading