Пишем блог на микросервисах – часть 4 сервис «Post»

от автора

Эта статья является продолжением истории написания demo-блога на микросервисах (предыдущие части можно почитать здесь: Часть 1 «Общее описание архитектуры», Часть 2 «API Gateway», Часть 3 «Сервис User»). В этой статье речь пойдет о реализации микросервиса Post (статьи).

Основной особенностью микросервиса является то, что он реализует различные виды связей с другими сервисами. Например, с сервисом Comments (комментарии) реализован тип связи один ко многим (у одной статьи может быть несколько комментариев), а с сервисами User и Category реализованы связи многое к одному (т.е. у одного пользователя может быть много статей и у одной категории может быть несколько статей).

С точки зрения функциональности в сервисе Post будут реализованы следующие методы:
— Логирование запросов к сервису и промежуточных состояния (механизм подробно описан в статье Часть 3 «Сервис User») с указанием TraceId (тот самый, который был выдан api-gw, см. Часть 2 «API Gateway»)
— Функции CRUD (создание, чтение, редактирование, удаление записи в БД — MongoDB).
— Функции поиска: поиск всех статей, поиск по категории, поиск по автору

Традиционно создание микросервиса начнем с его описания в протофайле

//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; } 

Далее генерим каркас микросервиса. Для этого переходим в корневой каталог проекта и выполняем команду sh ./bin/protogen.sh.

Супер! Большую часть работы за нас сделал кодогенератор, нам осталось только написать реализацию прикладных функций. Открываем файл ./services/post/functions.go и пишем реализацию.
Рассмотрим основные фрагменты функциии Create.

1. Парсим контекст вызова и достаем из него информацию о пользователе.

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

2. Проверяем параметры запроса и если они содержат недопустимые значения, возвращаем соответствующую ошибку.

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

3. Сохраняем Post в БД (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. Получаем Id созданной записи, добавляем ее к ответу и возвращаем ответ.

... 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 ... 

Ранее я упоминал, что сервис Post интересен своими связями с другими сервисами. Наглядно это демонстрирует метод Get (получить Post по заданному ID)
Для начала прочитаем из 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 } ... 

Здесь все более-менее просто. вначале преобразуем строку в ObjectID и далее используем его в filter для поиска записи.

Теперь нам нужно полученную запись Post обогатить данными об авторе. Для этого нужно сходить в сервис User и получить запись по заданному UserId. Сделать это можно следующим образом:

... //Запрос к сервису 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 ... 

Хочу обратить внимание, что я умышленно использую два разных термина User и Author, т.к. считаю, что они лежат в разных контекстах. User — это про логины/пароли аутентификацию и прочие атрибуты и функции так или иначе связанные с безопасностью и доступами. Author — это сущность про опубликованные посты, комментарии и прочее. Сущность Author рождается в контексте Post используя за основу данные из User. (надеюсь мне удалось объяснить разницу 😉

Следующим шагом вычитываем данные по связанным категориям из сервиса Category. Не уверен, что предлагаю оптимальный вариант (надеюсь сообщество поправит). Суть подхода следующая: делаем ОДИН запрос в сервис Category и вычитываем ВСЕ существующие категории, далее в сервисе Post выбираем только те категории, которые связаны с Post. Минус данного подхода — оверхэд по передаваемым данным, плюс — делаем всего один запрос. Т.к. кол-во категорий это определенно не зашкаливающая величина считаю что оверхэдом можно пренебречь.

... //Запрос к сервису 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) 	 }        }     } ... 

Следующее что нам следует сделать это получить все связанные комментарии. Здесь задача похожа на задачу с категориями, за исключением, что в случае с категориями Id связанных категорий у нас хранились в Post, в случае с комментариями наооборот Id родительского Post хранится непосредственно в дочерних комментариях. На самом деле это сильно упрощает задачу, т.к. все что нам нужно, это сделать запрос в сервис Comments с указанием родительского Post и обработать результат — в цикле добавить к Post все связанные PostComment

... //Запрос к сервису 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)    } ... 

И возвращаем собранный Post

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

В web интерфейсе у нас реализована навигация по категориям и по авторам. Т.е. когда пользователь кликает по категории ему отображается список всех статей, которые ссылаются на выбранную категорию. А когда кликает по автору, соответственно отображается список статей, где автором указан выбранный пользователь.

Для реализации этой функциональности в сервисе Post предусмотрены два метода:
GetPostCategory — возвращает структуру PostCategory, которая содержит ID, наименование категории и коллекцию связанных статей
GetAuthor — возвращает структуру Author котора содержит атрибуты пользователя (FirstName, LastName и т. п.) и коллекцию связанных Post.

Подробно описывать реализацию этих методов не буду дабы не повторяться. Они базируются на тех же фрагментах кода что были описаны выше.


ссылка на оригинал статьи https://habr.com/ru/company/X5RetailGroup/blog/481316/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *