Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Modelling your Data Layer in Go
Abhik Banerjee
Abhik Banerjee

Posted on • Originally published atabhik.hashnode.dev

Modelling your Data Layer in Go

There are a few statements any Go dev would have heard like:

  1. There is no perfect programming language.
  2. Go is modular by design.
  3. In Golang, pass-by-value is faster than the pass-by-reference.
  4. Go is the perfect programming language

And so on…

In this article, we pick up the modelling aspect of our Go application. While we are trying to follow the MVC architecture, there are some Gopher quirks that we have observed previously. This article will show how those translate to coding when dealing with data models and DB business logic wrappers.

Thedata package

In our case, we will keep all the model-related data and data transfer objects (DTOs) inside thedata package. This package will have 4 files as we will discuss below. This package is built on top of theconfig andutils packages which we discussed in the previous article.

User Profile Representation in Go

First, let’s have a look at theuser_model.go file. This file contains the user model which we will store in our MongoDB collection. Between lines 12 and 18, we define the fields of our user profile. Our user will have anId field which will be auto-generated by MongoDB. Unlike JS, JSON objects are not natively compatible with Go. Neither is MongoDB’s BSON. To facilitate interoperability, we need to tag the fields of our struct. Thebson tag denotes what the struct field will be called in the MongoDB notation while thejson tag says what the field will be called when the struct is marshaled into JSON.

packagedataimport("github.com/abhik-99/passwordless-login/pkg/config""go.mongodb.org/mongo-driver/bson""go.mongodb.org/mongo-driver/bson/primitive""go.mongodb.org/mongo-driver/mongo""go.mongodb.org/mongo-driver/mongo/options")typeUserstruct{Idprimitive.ObjectID`bson:"_id" json:"id"`Picstring`bson:"profilePic" json:"profilePic"`Namestring`bson:"name" json:"name"`Emailstring`bson:"email" json:"email"`Phonestring`bson:"phone" json:"phone"`}var(userCollection=config.Db.Collection("user-collection")ctx=config.MongoCtx)funcCreateNewUser(userCreateUserDTO)(*mongo.InsertOneResult,error){returnuserCollection.InsertOne(ctx,user)}funcGetAllPublicUserProfiles()([]PublicUserProfileDTO,error){varusers[]PublicUserProfileDTOcursor,err:=userCollection.Find(ctx,bson.M{})iferr!=nil{returnusers,err}iferr=cursor.All(ctx,&users);err!=nil{returnusers,nil}else{returnusers,err}}funcGetUserProfileById(idstring)(PublicFullUserProfileDTO,error){varuserPublicFullUserProfileDTOobId,err:=primitive.ObjectIDFromHex(id)iferr!=nil{returnuser,err}err=userCollection.FindOne(ctx,bson.M{"_id":obId}).Decode(&user)returnuser,err}funcUserLookupViaEmail(emailstring)(bool,string,error){varresultUserfilter:=bson.D{{Key:"email",Value:email}}projection:=bson.D{{Key:"_id",Value:1}}iferr:=userCollection.FindOne(ctx,filter,options.FindOne().SetProjection(projection)).Decode(&result);err!=nil{returnfalse,"",err}returntrue,result.Id.Hex(),nil}funcUserLookupViaPhone(phonestring)(bool,string,error){varresultUserfilter:=bson.D{{Key:"phone",Value:phone}}projection:=bson.D{{Key:"secret",Value:1},{Key:"counter",Value:1},{Key:"_id",Value:1}}iferr:=userCollection.FindOne(ctx,filter,options.FindOne().SetProjection(projection)).Decode(&result);err!=nil{returnfalse,"",err}returntrue,result.Id.Hex(),nil}funcUpdateUserProfile(idstring,uEditUserDTO)(*mongo.UpdateResult,error){ifobId,err:=primitive.ObjectIDFromHex(id);err!=nil{update:=bson.D{{Key:"$set",Value:u}}returnuserCollection.UpdateByID(ctx,obId,update)}else{returnnil,err}}funcDeleteUserProfile(idstring)(*mongo.DeleteResult,error){ifobId,err:=primitive.ObjectIDFromHex(id);err!=nil{returnuserCollection.DeleteOne(ctx,bson.M{"_id":obId})}else{returnnil,err}}
Enter fullscreen modeExit fullscreen mode

Next, between lines 20 and 23, we importDb from theconfig package. We then create a reference to theuser-collection MongoDB collection which we had created during the initialization phase and store that in theuserCollection variable. The mongo context is also imported and stored in thectx variable.

The first method we have is theCreateNewUser() function which takes in a user, inserts the data, and then returns the insertion result. It is a light wrapper around theInsertOne() function. After that, we have theGetAllPublicUserProfiles() function. This function queries all the users in theuser-collection. However, it does not return all the fields of every collection. It returns only those fields that are present in thePublicUserProfileDTO struct (struct shown below). This ensures that only a subset of fields is returned.

// Inside of user_dto.gotypePublicUserProfileDTOstruct{Idprimitive.ObjectID`bson:"_id" json:"id"`Picstring`bson:"profilePic" json:"profilePic"`Namestring`bson:"name" json:"name"`}
Enter fullscreen modeExit fullscreen mode

Keep in mind that we are going to keep all the concerned DTOs in one file. This means that all DTOs concerned with the Mongo User collection are going insideuser_dto.go file including the struct above. This limits the fields returned in the response.

But that is not the only way. You can just tell which fields to filter by and then which ones to return in the query itself. TheGetUserProfileById() is not that special – it just takes in an ID and then returns the full user profile which matches the ID. The DTO for the Full user profile is shown below.

// Inside user_dto.gotypePublicFullUserProfileDTOstruct{Idprimitive.ObjectID`bson:"_id" json:"id"`Picstring`bson:"profilePic" json:"profilePic"`Namestring`bson:"name" json:"name"`Emailstring`bson:"email" json:"email"`Phonestring`bson:"phone" json:"phone"`}
Enter fullscreen modeExit fullscreen mode

TheUserLookupViaEmail() is a bit special though. You can probably guess where it can be used. It takes in an email ID and then returns the ID of the user to whom that Email ID belongs. Pay close attention to line number 57. That is where we define which fields the query should return. This is another way to limit the number of fields returned in the response. We specify this projection in theoptions we pass to theFindOne() query.

Keep in mind that the ID which will be returned will not be astring. Invoking theHex() function on the returned MongoDB ID will turn it into astring which we can return.

TheUserLookupViaPhone() does something similar albeit with the phone number of a user. Since it is similar, we won’t be discussing it.

As for the other functions in theuser_model.go file, they are just present for completion’s sake and provide a full range of CRUD functionality. We will forego discussing them since they are fairly easy to understand if you have understood the function we discussed thus far.

Authentication-related modeling in Go

Next, we have theauth_model.go. In this section, we will focus on modeling the data layer so that we can interact with the Redis instance and save & check OTPs. Unlike MongoDB, we won’t have a struct decorator here since Redis is a key-value DB. That being said, there needs to be some structure to the model so that we can use it in our project with ease. The code given below is what we will have in ourauth_model.go file.

packagedataimport("time""github.com/abhik-99/passwordless-login/pkg/config")typeAuthstruct{UserIdstringOtpstring}var(redisDb=config.RdbrCtx=config.RedisCtx)func(a*Auth)SetOTPForUser()error{returnredisDb.Set(rCtx,a.UserId,a.Otp,30*time.Minute).Err()}func(a*Auth)CheckOTP()(bool,error){ifstoredOtp,err:=redisDb.Get(rCtx,a.UserId).Result();err!=nil{returnfalse,err}else{ifstoredOtp==a.Otp{returntrue,nil}}returnfalse,nil}
Enter fullscreen modeExit fullscreen mode

First, we define theAuth struct between lines 9 and 12.UserId field will be the key while theOtp will represent the corresponding value. As you might have guessed, we will map Mongo DB Object IDs to OTPs and store that in Redis. In lines 14 to 17 we import the Redis connection and context fromconfig package which we defined in the previous article.

We follow a different pattern in this kind of modelling. Unlike the previous user model, we define the methods with the struct as a receiver. So, when we initialize anAuth struct in our project, we can call theSetOTPForUser() function to set an auto-expiring OTP in Redis using the.Set() function or we may invoke theCheckOTP() function to find if OTP exists and matches the value inOtp field of theAuth struct we initialized. In the latter case, if OTP does not exist, then we can also assume that 30 minutes have passed and so the OTP has expired.

Next, we have the DTOs as shown in the code below. We will place them in theauth_dto.go file. As can be seen below, we have 3 DTO structs defined.LoginWithEmailDTO is used to decode the POST request body when email and OTP are sent.LoginWithPhoneDTO has similar usage but with a phone number. While theAccessTokenDTO is used for encoding the JWT access token in the response on successful authentication.

packagedatatypeLoginWithEmailDTOstruct{Emailstring`json:"email" validate:"required,email"`Otpstring`json:"otp" validate:"required"`}typeLoginWithPhoneDTOstruct{Phonestring`json:"phone" validate:"required,e164"`Otpstring`json:"otp" validate:"required"`}typeAccessTokenDTOstruct{AccessTokenstring`json:"access_token"`}
Enter fullscreen modeExit fullscreen mode

This completes the modeling phase of the project. We now haveconfig,utils, anddata packages ready. So, we can proceed to theroutes andcontroller packages where the crux of our business logic will be implemented. We will explore this in the next articles.

Conclusion

As is apparent from this article, Golang is pretty unopinionated when it comes to how one shouldarrange their files and whichconventions to choose from.

This freedom comes with a downside that can lead to poorly constructed applications. Sometimes, it can even lead tooverengineering which can just needlessly increase codebase complexity. The quality in such a case, largely depends on the developer’s coding practices and the conventions being followed by a project. These help reduce thechaos in the codebase.

In this article, we went over the data modelling aspect of our mini-project. Hope to see you in the next one. Until then, keep building awesome things and WAGMI!

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Web3 Dev, Foodie, Cats and Anime Lover rolled into one. Ping me up if you have any article topic recommendations ;)
  • Location
    Kolkata, West Bengal, India.
  • Joined

More fromAbhik Banerjee

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp