Rédaction d'un blog sur les microservices - Partie 4 Post Service

Cet article est une continuation de l'histoire de la rédaction d'un blog de démonstration sur les microservices (les parties précédentes peuvent être lues ici: Partie 1 «Description générale de l'architecture» , Partie 2 «API Gateway» , Partie 3 «Service User» ). Cet article se concentrera sur la mise en œuvre du microservice Post (articles).

La principale caractéristique du microservice est qu'il implémente différents types de connexions avec d'autres services. Par exemple, avec le service Commentaires (commentaires), le type de communication un-à-plusieurs est implémenté (un article peut avoir plusieurs commentaires) et les services Utilisateur et Catégorie ont des connexions plusieurs-à-un (c'est-à-dire qu'un utilisateur peut avoir plusieurs articles et un catégories peuvent être plusieurs articles).

En termes de fonctionnalité, les méthodes suivantes seront mises en œuvre dans le service postal:

  • Journalisation des demandes de service et des états intermédiaires (le mécanisme est décrit en détail dans la partie 3 «Service utilisateur») avec TraceId (identique à l'api-gw émis; voir la partie 2 «Passerelle API» )
  • Fonctions CRUD (créer, lire, éditer, supprimer des enregistrements dans la base de données - MongoDB).
  • Fonctions de recherche: rechercher tous les articles, rechercher par catégorie, rechercher par auteur

Traditionnellement, nous allons commencer à créer un microservice en le décrivant dans un profil.

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

Ensuite, nous générons le cadre du microservice. Pour ce faire, accédez au répertoire racine du projet et exécutez la commande sh ./bin/protogen.sh.

Super! La plupart du travail pour nous a été fait par le générateur de code, nous avons juste eu à écrire une implémentation des fonctions de l'application. Ouvrez le fichier ./services/post/functions.go et écrivez l'implémentation.

Considérez les principaux fragments de la fonction Créer.

1. Analysez le contexte de l'appel et obtenez des informations sur l'utilisateur.

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

2. Vérifiez les paramètres de la requête et s'ils contiennent des valeurs non valides, renvoyez l'erreur correspondante.

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

3. Enregistrez la publication dans la base de données (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. Nous recevons l'ID de l'enregistrement créé, nous l'ajoutons à la réponse et nous renvoyons la réponse.

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

J'ai mentionné plus tôt que le service postal est intéressant pour ses connexions avec d'autres services. Ceci est clairement démontré par la méthode Get (get Post par l'ID donné).

Tout d'abord, lisez le 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 } ... 

Ici, tout est plus ou moins simple. Tout d'abord, convertissez la chaîne en ObjectID, puis utilisez-la dans le filtre pour rechercher l'enregistrement.

Maintenant, nous devons enrichir l'enregistrement de publication résultant avec des données sur l'auteur. Pour ce faire, accédez au service utilisateur et obtenez un enregistrement pour l'ID utilisateur spécifié. Vous pouvez le faire comme suit:

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

Je tiens à noter que j'utilise délibérément deux termes différents utilisateur et auteur, car Je crois qu'ils se situent dans des contextes différents. L'utilisateur concerne l'authentification des noms d'utilisateur / mots de passe et d'autres attributs et fonctions d'une manière ou d'une autre liés à la sécurité et à l'accès. L'auteur est une entité sur les articles publiés, les commentaires, etc. L'entité auteur est née dans le contexte de la publication, en utilisant les données de l'utilisateur comme base. (J'espère avoir réussi à expliquer la différence;)

L'étape suivante consiste à lire les données des catégories connexes à partir du service de catégorie. Je ne suis pas sûr de proposer la meilleure option (j'espère que la communauté la corrige). L'essence de l'approche est la suivante: nous faisons UNE demande au service de catégorie et soustrayons TOUTES les catégories existantes, puis dans le service de poste, nous sélectionnons uniquement les catégories liées à la poste. L'inconvénient de cette approche est la surcharge pour les données transmises, plus - nous ne faisons qu'une seule demande. Parce que le nombre de catégories n'est certainement pas énorme, je pense que les frais généraux peuvent être négligés.

 ... //   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) } } } ... 

La prochaine chose que nous devrions faire est d'obtenir tous les commentaires connexes. Ici, la tâche est similaire à la tâche avec des catégories, sauf que dans le cas des catégories Id, les catégories liées ont été stockées dans Post, dans le cas des commentaires, en revanche, l'ID de la publication parent est stocké directement dans les commentaires enfants. En fait, cela simplifie considérablement la tâche, car tout ce que nous devons faire est de faire une demande au service Commentaires en indiquant le message parent et de traiter le résultat - dans une boucle, ajoutez au message tous les commentaires connexes

 ... //   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) } ... 

Et renvoyez le message collecté

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

Dans l'interface Web, nous avons implémenté la navigation par catégorie et par auteur. C'est-à-dire lorsqu'un utilisateur clique sur une catégorie, une liste de tous les articles liés à la catégorie sélectionnée s'affiche. Et lorsqu'il clique sur l'auteur, une liste d'articles s'affiche en conséquence, où l'utilisateur sélectionné est indiqué par l'auteur.

Il existe deux méthodes pour implémenter cette fonctionnalité dans le service Post:

GetPostCategory - Renvoie une structure PostCategory qui contient l'ID, le nom de la catégorie et la collection d'articles associés
GetAuthor - Renvoie une structure d'auteur qui contient des attributs utilisateur (FirstName, LastName, etc.) et une collection de messages associés.

Je ne décrirai pas en détail la mise en œuvre de ces méthodes afin de ne pas les répéter. Ils sont basés sur les mêmes fragments de code que ceux décrits ci-dessus.

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


All Articles