Entities and data access

To persist data in Google Cloud Datastore, we need a struct to represent each entity. These entity structures will be serialized and deserialized when we save and load data through the datastore API. We can add helper methods to perform the interactions with the data store, which is a nice way to keep such functionality physically close to the entities themselves. For example, we will model an answer with a struct called Answer and add a Create method that in turn calls the appropriate function from the datastore package. This prevents us from bloating our HTTP handlers with lots of data access code and allows us to keep them clean and simple instead.

One of the foundation blocks of our application is the concept of a question. A question can be asked by a user and answered by many. It will have a unique ID so that it is addressable (referable in a URL), and we'll store a timestamp of when it was created.

Create a new file inside answersapp called questions.go and add the following struct function:

type Question struct { 
  Key *datastore.Key `json:"id" datastore:"-"` 
  CTime time.Time `json:"created"` 
  Question string `json:"question"` 
  User UserCard `json:"user"` 
  AnswersCount int `json:"answers_count"` 
} 

The structure describes a question in our application. Most of it will seem quite obvious, as we've done similar things in the previous chapters. The UserCard struct represents a denormalized User entity, both of which we'll add later.

Tip

You can import the datastore package in your Go project using this: import "google.golang.org/appengine/datastore"

It's worth spending a little time understanding the datastore.Key type.

Keys in Google Cloud Datastore

Every entity in Datastore has a key, which uniquely identifies it. They can be made up of either a string or an integer depending on what makes sense for your case. You are free to decide the keys for yourself or let Datastore automatically assign them for you; again, your use case will usually decide which is the best approach to take and we'll explore both in this chapter.

Keys are created using the datastore.NewKey and datastore.NewIncompleteKey functions and are used to put and get data into and out of Datastore via the datastore.Get and datastore.Put functions.

In Datastore, keys and entity bodies are distinct, unlike in MongoDB or SQL technologies, where it is just another field in the document or record. This is why we are excluding Key from our Question struct with the datastore:"-" field tag. Like the json tags, this indicates that we want Datastore to ignore the Key field altogether when it is getting and putting data.

Keys may optionally have parents, which is a nice way of grouping associated data together and Datastore makes certain assurances about such groups of entities, which you can read more about in the Google Cloud Datastore documentation online.

Putting data into Google Cloud Datastore

Before we save data into Datastore, we want to ensure that our question is valid. Add the following method underneath the Question struct definition:

func (q Question) OK() error { 
  if len(q.Question) < 10 { 
    return errors.New("question is too short") 
  } 
  return nil 
} 

The OK function will return an error if something is wrong with the question, or else it will return nil. In this case, we just check to make sure the question has at least 10 characters.

To persist this data in the data store, we are going to add a method to the Question struct itself. At the bottom of questions.go, add the following code:

func (q *Question) Create(ctx context.Context) error { 
  log.Debugf(ctx, "Saving question: %s", q.Question) 
  if q.Key == nil { 
    q.Key = datastore.NewIncompleteKey(ctx, "Question", nil) 
  } 
  user, err := UserFromAEUser(ctx) 
  if err != nil { 
    return err 
  } 
  q.User = user.Card() 
  q.CTime = time.Now() 
  q.Key, err = datastore.Put(ctx, q.Key, q) 
  if err != nil { 
    return err 
  } 
  return nil 
} 

The Create method takes a pointer to Question as the receiver, which is important because we want to make changes to the fields.

Note

If the receiver was (q Question) without *, we would get a copy of the question rather than a pointer to it, and any changes we made to it would only affect our local copy and not the original Question struct itself.

The first thing we do is use log (from the https://godoc.org/google.golang.org/appengine/log package) to write a debug statement saying we are saving the question. When you run your code in a development environment, you will see this appear in the terminal; in production, it goes into a dedicated logging service provided by Google Cloud Platform.

If the key is nil (that means this is a new question), we assign an incomplete key to the field, which informs Datastore that we want it to generate a key for us. The three arguments we pass are context.Context (which we must pass to all datastore functions and methods), a string describing the kind of entity, and the parent key; in our case, this is nil.

Once we know there is a key in place, we call a method (which we will add later) to get or create User from an App Engine user and set it to the question and then set the CTime field (created time) to time.Now, timestamping the point at which the question was asked.

One we have our Question function in good shape, we call datastore.Put to actually place it inside the data store. As usual, the first argument is context.Context, followed by the question key and the question entity itself.

Since Google Cloud Datastore treats keys as separate and distinct from entities, we have to do a little extra work if we want to keep them together in our own code. The datastore.Put method returns two arguments: the complete key and error. The key argument is actually useful because we're sending in an incomplete key and asking the data store to create one for us, which it does during the put operation. If successful, it returns a new datastore.Key object to us, representing the completed key, which we then store in our Key field in the Question object.

If all is well, we return nil.

Add another helper to update an existing question:

func (q *Question) Update(ctx context.Context) error { 
  if q.Key == nil { 
    q.Key = datastore.NewIncompleteKey(ctx, "Question", nil) 
  } 
  var err error 
  q.Key, err = datastore.Put(ctx, q.Key, q) 
  if err != nil { 
    return err 
  } 
  return nil 
} 

This method is very similar except that it doesn't set the CTime or User fields, as they will already have been set.

Reading data from Google Cloud Datastore

Reading data is as simple as putting it with the datastore.Get method, but since we want to maintain keys in our entities (and datastore methods don't work like that), it's common to add a helper function like the one we are going to add to questions.go:

func GetQuestion(ctx context.Context, key *datastore.Key) 
(*Question, error) { 
  var q Question 
  err := datastore.Get(ctx, key, &q) 
  if err != nil { 
    return nil, err 
  } 
  q.Key = key 
  return &q, nil 
} 

The GetQuestion function takes context.Context and the datastore.Key method of the question to get. It then does the simple task of calling datastore.Get and assigning the key to the entity before returning it. Of course, errors are handled in the usual way.

This is a nice pattern to follow so that users of your code know that they never have to interact with datastore.Get and datastore.Put directly but rather use the helpers that can ensure the entities are properly populated with the keys (along with any other tweaks that they might want to do before saving or after loading).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.24.30