Menulis Blog Microservice - Bagian 4 Layanan Pos

Artikel ini merupakan kelanjutan dari sejarah penulisan blog demo di layanan microser (bagian sebelumnya dapat dibaca di sini: Bagian 1 "Deskripsi umum arsitektur" , Bagian 2 "API Gateway" , Bagian 3 "Pengguna Layanan" ). Artikel ini akan fokus pada implementasi microservice Post (artikel).

Fitur utama dari layanan mikro adalah bahwa ia mengimplementasikan berbagai jenis koneksi dengan layanan lain. Misalnya, dengan layanan Komentar (komentar), jenis komunikasi satu-ke-banyak diterapkan (satu artikel mungkin memiliki beberapa komentar), dan layanan Pengguna dan Kategori memiliki koneksi banyak-ke-satu (mis., Satu pengguna dapat memiliki banyak artikel dan satu kategori dapat berupa beberapa artikel).

Dalam hal fungsionalitas, metode berikut akan diimplementasikan dalam layanan Pos:

  • Pencatatan permintaan layanan dan status perantara (mekanisme dijelaskan secara rinci di Bagian 3 "Layanan Pengguna") dengan TraceId (yang sama dikeluarkan oleh api-gw, lihat Bagian 2 "API Gateway" )
  • Fungsi CRUD (membuat, membaca, mengedit, menghapus catatan dalam database - MongoDB).
  • Fungsi pencarian: mencari semua artikel, mencari berdasarkan kategori, mencari berdasarkan penulis

Secara tradisional, kami akan mulai membuat microservice dengan menjelaskannya dalam protofile.

//post.proto yntax = "proto3"; package protobuf; import "google/api/annotations.proto"; //   Post service PostService { //  rpc Create (CreatePostRequest) returns (CreatePostResponse) { option (google.api.http) = { post: "/api/v1/post" }; } //  rpc Update (UpdatePostRequest) returns (UpdatePostResponse) { option (google.api.http) = { post: "/api/v1/post/{Slug}" }; } //  rpc Delete (DeletePostRequest) returns (DeletePostResponse) { option (google.api.http) = { delete: "/api/v1/post/{Slug}" }; } //      rpc GetPostCategory (GetPostCategoryRequest) returns (GetPostCategoryResponse) { //     option (google.api.http) = { get: "/api/v1/post/category/{Slug}" }; } //   rpc Find (FindPostRequest) returns (FindPostResponse) { option (google.api.http) = { get: "/api/v1/post" }; } //     rpc Get (GetPostRequest) returns (GetPostResponse) { option (google.api.http) = { get: "/api/v1/post/{Slug}" }; } //   rpc GetAuthor (GetAuthorRequest) returns (GetAuthorResponse) { //    SLUG option (google.api.http) = { get: "/api/v1/author/{Slug}" }; } //   rpc FindAuthors (FindAuthorRequest) returns (FindAuthorResponse) { //   option (google.api.http) = { get: "/api/v1/author" }; } } //--------------------------------------------------------------- // CREATE //--------------------------------------------------------------- message CreatePostRequest { string Title = 1; string SubTitle = 2; string Content = 3; string Categories = 4; } message CreatePostResponse { Post Post = 1; } //--------------------------------------------------------------- // UPDATE //--------------------------------------------------------------- message UpdatePostRequest { string Slug = 1; string Title = 2; string SubTitle = 3; string Content = 4; int32 Status = 5; string Categories = 6; } message UpdatePostResponse { int32 Status =1; } //--------------------------------------------------------------- // DELETE //--------------------------------------------------------------- message DeletePostRequest { string Slug = 1; } message DeletePostResponse { int32 Status =1; } //--------------------------------------------------------------- // GET //--------------------------------------------------------------- message GetPostRequest { string Slug = 1; } message GetPostResponse { Post Post = 1; } //--------------------------------------------------------------- // FIND POST //--------------------------------------------------------------- message FindPostRequest { string Slug = 1; } message FindPostResponse { repeated Post Posts = 1; } //--------------------------------------------------------------- // GET AUTHOR //--------------------------------------------------------------- message GetAuthorRequest { string Slug = 1; } message GetAuthorResponse { Author Author = 1; } //--------------------------------------------------------------- // FIND AUTHOR //--------------------------------------------------------------- message FindAuthorRequest { string Slug = 1; } message FindAuthorResponse { repeated Author Authors = 1; } //--------------------------------------------------------------- // GET CATEGORY //--------------------------------------------------------------- message GetPostCategoryRequest { string Slug = 1; } message GetPostCategoryResponse { PostCategory Category = 1; } //--------------------------------------------------------------- // POST //--------------------------------------------------------------- message Post { string Slug = 1; string Title = 2; string SubTitle = 3; string Content = 4; string UserId = 5; int32 Status = 6; string Src = 7; Author Author = 8; string Categories = 9; repeated PostCategory PostCategories = 10; string Comments = 11; repeated PostComment PostComments = 12; } //--------------------------------------------------------------- // Author //--------------------------------------------------------------- message Author { string Slug = 1; string FirstName = 2; string LastName = 3; string SrcAvatar = 4; string SrcCover = 5; repeated Post Posts = 6; } //--------------------------------------------------------------- // PostCategory //--------------------------------------------------------------- message PostCategory { string Slug = 1; string Name = 2; repeated Post Posts = 3; } //--------------------------------------------------------------- // PostComment //--------------------------------------------------------------- message PostComment { string Slug = 1; string Content = 2; Author Author = 3; } 

Selanjutnya, kami membuat kerangka kerja layanan-mikro. Untuk melakukan ini, buka direktori root proyek dan jalankan perintah sh ./bin/protogen.sh.

Hebat! Sebagian besar pekerjaan bagi kami dilakukan oleh pembuat kode, kami hanya perlu menulis implementasi fungsi aplikasi. Buka file ./services/post/functions.go dan tulis implementasinya.

Pertimbangkan fragmen utama dari fungsi Buat.

1. Parsing konteks panggilan dan dapatkan informasi tentang pengguna darinya.

 ... md,_:=metadata.FromIncomingContext(ctx) var userId string if len(md["user-id"])>0{ userId=md["user-id"][0] } ... 

2. Periksa parameter kueri dan jika mengandung nilai yang tidak valid, kembalikan kesalahan yang sesuai.

 ... if in.Title==""{ return nil,app.ErrTitleIsEmpty } ... 

3. Simpan Posting di database (mongoDB).

 ... collection := o.DbClient.Database("blog").Collection("posts") post:=&Post{ Title:in.Title, SubTitle:in.SubTitle, Content:in.Content, Status:app.STATUS_NEW, UserId:userId, Categories:in.Categories, } insertResult, err := collection.InsertOne(context.TODO(), post) if err != nil { return nil,err } ... 

4. Kami menerima Id dari catatan yang dibuat, kami menambahkannya ke jawaban dan kami mengembalikan jawabannya.

 ... if oid, ok := insertResult.InsertedID.(primitive.ObjectID); ok { post.Slug=fmt.Sprintf("%s",oid.Hex()) }else { err:=app.ErrInsert return out,err } out.Post=post return out,nil ... 

Saya sebutkan sebelumnya bahwa layanan Post menarik karena hubungannya dengan layanan lain. Ini jelas ditunjukkan oleh metode Dapatkan (dapatkan Posting dengan ID yang diberikan).

Pertama, baca dari mongoDB Post:

 ... collection := o.DbClient.Database("blog").Collection("posts") post:=&Post{} id, err := primitive.ObjectIDFromHex(in.Slug) if err != nil { return nil,err } filter:= bson.M{"_id": id} err= collection.FindOne(context.TODO(), filter).Decode(post) if err != nil { return nil,err } ... 

Semuanya kurang lebih sederhana di sini. Pertama, konversikan string ke ObjectID dan kemudian gunakan dalam filter untuk mencari catatan.

Sekarang kita perlu memperkaya catatan Post yang dihasilkan dengan data tentang penulis. Untuk melakukan ini, pergi ke layanan Pengguna dan dapatkan catatan untuk UserId yang ditentukan. Anda dapat melakukan ini sebagai berikut:

 ... //   User var header, trailer metadata.MD resp, err := o.UserService.Get( getCallContext(ctx), &userService.GetUserRequest{Slug:post.UserId}, grpc.Header(&header), //       grpc.Trailer(&trailer), //       ) if err != nil { return nil,err } author:=&Author{ Slug:resp.User.Slug, FirstName:resp.User.FirstName, LastName:resp.User.LastName, SrcAvatar:SRC_AVATAR, //TODO -  SrcCover:SRC_COVER, //TODO -  } post.Author=author ... 

Saya ingin mencatat bahwa saya sengaja menggunakan dua istilah yang berbeda, Pengguna dan Penulis, karena Saya percaya bahwa mereka ada dalam konteks yang berbeda. Pengguna adalah tentang nama pengguna / otentikasi kata sandi dan atribut dan fungsi lainnya dengan satu atau lain cara yang terkait dengan keamanan dan akses. Penulis adalah entitas tentang posting yang dipublikasikan, komentar, dan banyak lagi. Entitas Penulis dilahirkan dalam konteks Posting, menggunakan data dari Pengguna sebagai dasar. (Saya harap saya berhasil menjelaskan perbedaannya;)

Langkah selanjutnya adalah membaca data untuk kategori terkait dari layanan Kategori. Saya tidak yakin bahwa saya mengusulkan opsi terbaik (saya harap masyarakat memperbaikinya). Inti dari pendekatan ini adalah sebagai berikut: kami membuat SATU permintaan ke layanan Kategori dan mengurangi SEMUA kategori yang ada, kemudian di layanan Pos kami hanya memilih kategori yang terkait dengan Posting. Kelemahan dari pendekatan ini adalah overhead untuk data yang dikirimkan, ditambah - kami hanya membuat satu permintaan. Karena jumlah kategori jelas bukan jumlah yang besar, saya pikir overhead bisa diabaikan.

 ... //   Category, JOIN category respCategory,err:=o.CategoryService.Find( getCallContext(ctx), &categoryService.FindCategoryRequest{}, ) if err != nil { return out,err } for _, category:= range respCategory.Categories { for _, category_slug:= range strings.Split(post.Categories,",") { if category.Slug==category_slug{ postCategor:=&PostCategory{ Slug:category.Slug, Name:category.Name, } post.PostCategories=append(post.PostCategories,postCategor) } } } ... 

Hal selanjutnya yang harus kita lakukan adalah mendapatkan semua komentar terkait. Di sini, tugasnya mirip dengan tugas dengan kategori, kecuali bahwa dalam kasus Id kategori, kategori terkait disimpan di Post, dalam kasus komentar, di sisi lain, ID dari induk Postingan disimpan langsung dalam komentar anak. Bahkan, ini sangat menyederhanakan tugas, karena yang perlu kita lakukan hanyalah membuat permintaan ke layanan Komentar yang menunjukkan induk Post dan memproses hasilnya - dalam satu lingkaran, tambahkan ke Posting semua PostComment terkait

 ... //   Comments, JOIN comments respComment,err:=o.CommentService.Find( getCallContext(ctx), &commentService.FindCommentRequest{PostId:in.Slug}, ) if err != nil { return out,err } for _, comment:= range respComment.Comments { postComment:=&PostComment{ Slug:comment.Slug, Content:comment.Content, } post.PostComments=append(post.PostComments,postComment) } ... 

Dan kembalikan Pos yang dikumpulkan

 ... out.Post=post return out,nil ... 

Di antarmuka web, kami telah menerapkan navigasi berdasarkan kategori dan oleh penulis. Yaitu ketika pengguna mengklik suatu kategori, daftar semua artikel yang menautkan ke kategori yang dipilih ditampilkan. Dan ketika dia mengklik penulis, daftar artikel ditampilkan sesuai, di mana pengguna yang dipilih ditunjukkan oleh penulis.

Ada dua metode untuk menerapkan fungsi ini di layanan Post:

GetPostCategory - Mengembalikan struktur PostCategory yang berisi ID, nama kategori, dan koleksi artikel terkait
GetAuthor - Mengembalikan struktur Penulis yang berisi atribut pengguna (FirstName, LastName, dll.) Dan koleksi Posting terkait.

Saya tidak akan menjelaskan secara rinci implementasi metode ini agar tidak terulang. Mereka didasarkan pada fragmen kode yang sama yang dijelaskan di atas.

Source: https://habr.com/ru/post/id481316/


All Articles