Основной особенностью микросервиса является то, что он реализует различные виды связей с другими сервисами. Например, с сервисом 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/
Добавить комментарий