So here is where you start, the Python Datastore API. There is a lot of stuff there, I have only scratched the surface. The GAE model is built on Bigtable, a technology I pointed out a few posts ago. For a good high level explanation of how the Bigtable model is different from an RDBMS, check out this groovie post. Like anything, the best way to learn this stuff is just to dive in and do it. I added persistance using the GAE datastore to my running application. This is the biggest piece so far. Here is how I went about it.
Let me review the application I am writing. A running training log application. Very simple, login, add your daily run, list runs, logout. That is it. So to do this I created these models.
First a runner. A runner is a subclass of type Model. It lives in the google.appengine.ext.db package, which means it gets all the app engine persistance goodness included for free. There are other types of database models to use, but this seemed the easiest for a first try. My runner includes one property, the user as a UserProperty. UserProperty is a really cool class that holds a Google user account, built in!
class Runner(db.Model):
user = db.UserProperty()
class Runner(db.Model):
user = db.UserProperty()
I really didn't need a runner model. You will see when I define a run below, I could have just as well put the runner in that model. In fact, Bigtable sort of encourages you not to normalize the two classes the way I did. Still, I kept it this way for a couple of reasons. One, I wanted to learn how this parent/child grouping works (described below) and Bigtable will do some optimizations on parent/child grouping so this seemed like a good thing to me.
Here is my run class, again a db Model. It very simply holds the three properties I am interested in (name of run, distance, duration) and includes a time stamp that is auto created for us.
class Run(db.Model):
name = db.StringProperty()
distance = db.StringProperty()
duration = db.StringProperty()
date = db.DateTimeProperty(auto_now_add=True)
Simple! Now the cool part, writing and reading these things. I wrote a simple web page that provides a form for this data. After logging in, the user can enter data in the form and submit it. This is the class that saves the data after submission.
class RunLog(webapp.RequestHandler):
def post(self):
# Look for the current runner in the datastore first
runner = Runner.all().filter('user =',users.get_current_user()).get()
if not runner:
runner = Runner()
runner.user = users.get_current_user()
runner.put()
# Save the run when we have a runner parent
run = Run(parent=runner)
run.name = self.request.get('name')
run.distance = self.request.get('distance')
run.duration = self.request.get('duration')
run.put()
self.redirect('/listRuns')
Note a few things. I first look to see if this runner has been here and saved runs before. If so, I use that runner as the "parent" of the run. This creates an Entity Group and according to what I read is intended to be used with data that is very transactional. I can use transactions to update and manipulate all the data in this entity group, useful, but not so much for my app. Again, I wanted to see how this parent/child thing works first, even if it is easier to just include the runner in the run class itself. If I don't find a current "runner" I will create a new one and save the runs with that runner.
I did read one nice thing about Entity groups. Apparently Bigtable with provide a bit of an optimization and keep all data related to an Entity group physically close together. This should mean consistent response times for all data in your Entity group if your users are scattered across the globe. Seemed cool to me.
Next, reading. This actually comes first in the web design since the data is presented on the welcome page if you are logged in.
class MainPage(webapp.RequestHandler):
def get(self):
nickname = None
avatar = None
run = None
runner = None
# Check for current user, populate data if found
if users.get_current_user():
runner = Runner.all().filter('user =',users.get_current_user()).get()
if runner:
run = Run.all().ancestor(runner)
nickname = users.get_current_user().nickname()
avatar = gravatar(users.get_current_user().email())
template_values = {
'runner': nickname,
'avatar': avatar,
'runs': run,
}
path = os.path.join(os.path.dirname(__file__), 'runs.html')
self.response.out.write(template.render(path, template_values))
You can see that I am checking for a current user, if so, try to lookup to see if that user is a "runner" in our system. If this person is a runner, get all of their runs using the "ancestor" call on the Query class that comes back from a call to Run.all(). This will filter on only runs that this runner has entered because we made the parent connection in the RunLog class. Then I lookup a Gravatar if the user has one, and set the values for the template to render.
It took me a long time to get there, but that seems to me a basic setup. I feel like I am at about step 1 understanding Bigtable and GAE persistence. It is a new and different model, so any help is appreciated!
Here is my run class, again a db Model. It very simply holds the three properties I am interested in (name of run, distance, duration) and includes a time stamp that is auto created for us.
class Run(db.Model):
name = db.StringProperty()
distance = db.StringProperty()
duration = db.StringProperty()
date = db.DateTimeProperty(auto_now_add=True)
Simple! Now the cool part, writing and reading these things. I wrote a simple web page that provides a form for this data. After logging in, the user can enter data in the form and submit it. This is the class that saves the data after submission.
class RunLog(webapp.RequestHandler):
def post(self):
# Look for the current runner in the datastore first
runner = Runner.all().filter('user =',users.get_current_user()).get()
if not runner:
runner = Runner()
runner.user = users.get_current_user()
runner.put()
# Save the run when we have a runner parent
run = Run(parent=runner)
run.name = self.request.get('name')
run.distance = self.request.get('distance')
run.duration = self.request.get('duration')
run.put()
self.redirect('/listRuns')
Note a few things. I first look to see if this runner has been here and saved runs before. If so, I use that runner as the "parent" of the run. This creates an Entity Group and according to what I read is intended to be used with data that is very transactional. I can use transactions to update and manipulate all the data in this entity group, useful, but not so much for my app. Again, I wanted to see how this parent/child thing works first, even if it is easier to just include the runner in the run class itself. If I don't find a current "runner" I will create a new one and save the runs with that runner.
I did read one nice thing about Entity groups. Apparently Bigtable with provide a bit of an optimization and keep all data related to an Entity group physically close together. This should mean consistent response times for all data in your Entity group if your users are scattered across the globe. Seemed cool to me.
Next, reading. This actually comes first in the web design since the data is presented on the welcome page if you are logged in.
class MainPage(webapp.RequestHandler):
def get(self):
nickname = None
avatar = None
run = None
runner = None
# Check for current user, populate data if found
if users.get_current_user():
runner = Runner.all().filter('user =',users.get_current_user()).get()
if runner:
run = Run.all().ancestor(runner)
nickname = users.get_current_user().nickname()
avatar = gravatar(users.get_current_user().email())
template_values = {
'runner': nickname,
'avatar': avatar,
'runs': run,
}
path = os.path.join(os.path.dirname(__file__), 'runs.html')
self.response.out.write(template.render(path, template_values))
You can see that I am checking for a current user, if so, try to lookup to see if that user is a "runner" in our system. If this person is a runner, get all of their runs using the "ancestor" call on the Query class that comes back from a call to Run.all(). This will filter on only runs that this runner has entered because we made the parent connection in the RunLog class. Then I lookup a Gravatar if the user has one, and set the values for the template to render.
It took me a long time to get there, but that seems to me a basic setup. I feel like I am at about step 1 understanding Bigtable and GAE persistence. It is a new and different model, so any help is appreciated!