In this installment of this tutorial, we will show you how to create a skeleton Schevo application and a simple database schema. As you update the schema, we will show you how to explore the database using the Python interpreter. Here is what the final schema will look like:
"""Schema for MovieReviews."""
from schevo.schema import *
schevo.schema.prep(locals())
class SchevoIcon(E.Entity):
_hidden = True
name = f.string()
data = f.image()
_key(name)
class Actor(E.Entity):
name = f.string()
_key(name)
def x_movies(self):
return [casting.movie
for casting in self.m.movie_castings()]
class Director(E.Entity):
name = f.string()
_key(name)
class Movie(E.Entity):
title = f.string()
release_date = f.date()
director = f.entity('Director')
description = f.string(multiline=True, required=False)
_key(title)
def x_actors(self):
return [casting.actor
for casting in self.m.movie_castings()]
def __unicode__(self):
return u'%s (%i)' % (self.title, self.release_date.year)
class MovieCasting(E.Entity):
movie = f.entity('Movie', CASCADE)
actor = f.entity('Actor')
_key(movie, actor)
E.Actor._sample = [
('Keanu Reeves', ),
('Winona Ryder', ),
]
E.Director._sample = [
('Richard Linklater', ),
('Stephen Herek', ),
('Tim Burton', ),
]
E.Movie._sample = [
('A Scanner Darkly', '2006-07-28', ('Richard Linklater', ),
DEFAULT),
("Bill & Ted's Excellent Adventure", '1989-02-17',
('Stephen Herek', ), DEFAULT),
('Edward Scissorhands', '1990-12-14', ('Tim Burton', ),
DEFAULT),
]
E.MovieCasting._sample = [
(('A Scanner Darkly', ), ('Keanu Reeves', )),
(('A Scanner Darkly', ), ('Winona Ryder', )),
(("Bill & Ted's Excellent Adventure", ), ('Keanu Reeves', )),
(('Edward Scissorhands', ), ('Winona Ryder', )),
]
If you have not done so already, set up a Schevo environment as described in Getting Started With Schevo.
This tutorial is also a doctest, so let us set up doctest environment:
>>> from schevo.test import DocTest
For those not familiar with doctests, please note the following:
At a shell prompt, change to a directory that you want your new application in, then use the paster tool to create a new Schevo application:
$ paster create --template=schevo MovieReviews # [1]
$ cd MovieReviews # [2]
$ python setup.py develop # [3]
In your favorite text editor, open the file moviereviews/schema/moviereviews_001.py. This is the filename convention that Schevo uses when creating and evolving databases. The 001 represents the version number of the schema.
The prefix, while typically the same as the name of the top-level package of your project (in this case, moviereviews), can be anything you like, although only one prefix may be used in a schema package.
In the schema you will see the following lines near the top. These prepare the module namespace with the necessary “ingredients” for the Schevo schema syntax:
>>> schema = '''
... from schevo.schema import * # [1]
... schevo.schema.prep(locals()) # [2]
... '''
The default schema also has a class called SchevoIcon. We’re not going to take advantage of this in this tutorial, but let’s explore this class a little bit as it will give us a template for the other classes you’ll add to your schema:
>>> schema += '''
... class SchevoIcon(E.Entity): # [1]
... """Stores icons for Schevo database and application objects."""
...
... _hidden = True # [2]
...
... name = f.string() # [3]
... data = f.image() # [4]
...
... _key(name) # [5]
... '''
We’ve introduced some terms that are not necessarily unique to Schevo, but may not be familiar for those accustomed to working with SQL. Here are some basic definitions of those terms. We’ve left out some details to keep this tutorial simple.
For more detail, visit the Schevo Glossary.
Add the following class to the database schema:
>>> schema += '''
... class Movie(E.Entity):
...
... title = f.string()
... release_date = f.date()
... description = f.string(required=False,
... multiline=True) # [1], [2]
...
... _key(title) # [3]
... '''
Save the moviereviews_001.py file. At a shell prompt, use the schevo tool to create a new database with the application’s schema:
$ schevo db create --app=moviereviews sample.db
That’s it!
At this point, the sample.db file contains your new database, including the database schema itself.
For the doctest, let us create an in-memory database with the current schema:
>>> t = DocTest(schema)
>>> db = t.db
The schevo tool gives you a handy way to work with a database using a Python interpreter. We recommend installing IPython (just run "easy_install IPython") for a very comfortable experience.
At a shell prompt, open the database:
$ schevo shell sample.db
A variable db, representing the open database, is automatically inserted into the namespace of the interactive session.
All changes to a Schevo database are done via transaction objects that are responsible for ensuring that changes made to the database follow the rules you specify. See Schevo Transactions for further discussion about this design decision and the benefits you gain from it.
Make a new create transaction for creating a new Movie entity. Assign values to fields, then tell the database to execute the transaction:
>>> tx = db.Movie.t.create() # [1], [2], [3]
>>> tx.title = 'A Scanner Darkley' # [4]
>>> tx.release_date = '2006-07-28' # [5]
>>> movie = db.execute(tx) # [6]
Inspecting the field values of the entity shows that the proper information was stored in the database:
>>> movie.title
u'A Scanner Darkley'
>>> movie.release_date
datetime.date(2006, 7, 28)
Oops! There’s a typo in the title of the movie.
The movie entity object has a t namespace as well. By default, there are update and delete methods in an entity’s t namespace.
Create an update transaction object, correct the name, then execute the transaction and inspect the result:
>>> tx = movie.t.update()
>>> tx.title = 'A Scanner Darkly'
>>> movie = db.execute(tx)
>>> movie.title
u'A Scanner Darkly'
At this point, you may notice that if you print the movie object itself, it gives you a label based off the first key you define for the extent:
>>> print movie
A Scanner Darkly
Suppose in a user interface we would like to include the year the movie was released whenever a summary of a movie entity is to be displayed.
Exit the Python session, then edit moviereviews_001.py again and add the __unicode__ method to the Movie class:
>>> schema += '''
... class Movie(E.Entity):
...
... title = f.string()
... release_date = f.date()
... description = f.string(multiline=True, required=False)
...
... _key(title)
...
... def __unicode__(self):
... return u'%s (%i)' % (self.title, self.release_date.year)
... '''
At a shell prompt, update the existing database using the new schema, then open a Python session again:
$ schevo db update --app=moviereviews sample.db
$ schevo shell sample.db
For the doctest, let us perform the equivalent operation:
>>> t.update(schema)
>>> db = t.db
Find the movie, and inspect it:
>>> movie = db.Movie.findone(title='A Scanner Darkly') # [1]
>>> movie # [2]
<Movie entity oid:1 rev:1>
>>> print movie # [3]
A Scanner Darkly (2006)
Schevo likes to make things easy for humans to read, so you can go beyond thinking of it as a “unicode representation” of something, and think of it instead as a singular or plural label of an object. See Object Labels for further discussion about this feature.
Schevo has many relational-like capabilities, and it makes it easy to manage those.
We’ll flex this by having our database keep track of the director of each movie, and the actors that are in each movie. Close your Python session, and modify the schema to add the Actor, Director, and MovieCasting classes, and to modify the Movie class.
Wait! Before we get started, think about how much data we’ll have to populate with those transaction objects. Schevo allows you to change the structure of a database within the same schema version to some degree to support rapid development. Wouldn’t it also be nice to place some sample data directly in the schema?
Schevo lets you do that, so take advantage of it when you modify the schema by assigning _sample data lists to each entity class:
>>> schema += '''
... class Actor(E.Entity):
...
... name = f.string()
...
... _key(name)
...
...
... class Director(E.Entity):
...
... name = f.string()
...
... _key(name)
...
...
... class Movie(E.Entity):
...
... title = f.string()
... release_date = f.date()
... director = f.entity('Director')
... description = f.string(multiline=True, required=False)
...
... _key(title)
...
... def __unicode__(self):
... return u'%s (%i)' % (self.title, self.release_date.year)
...
...
... class MovieCasting(E.Entity):
...
... movie = f.entity('Movie', CASCADE)
... actor = f.entity('Actor')
...
... _key(movie, actor)
...
... def __unicode__(self):
... return u'%s :: %s' (self.movie, self.actor)
...
...
... E.Actor._sample = [
... ('Keanu Reeves', ),
... ('Winona Ryder', ),
... ]
...
... E.Director._sample = [
... ('Richard Linklater', ),
... ('Stephen Herek', ),
... ('Tim Burton', ),
... ]
...
... E.Movie._sample = [
... ('A Scanner Darkly', '2006-07-28', ('Richard Linklater', ),
... DEFAULT),
... ("Bill & Ted's Excellent Adventure", '1989-02-17',
... ('Stephen Herek', ), DEFAULT),
... ('Edward Scissorhands', '1990-12-14', ('Tim Burton', ),
... DEFAULT),
... ]
...
... E.MovieCasting._sample = [
... (('A Scanner Darkly', ), ('Keanu Reeves', )),
... (('A Scanner Darkly', ), ('Winona Ryder', )),
... (("Bill & Ted's Excellent Adventure", ), ('Keanu Reeves', )),
... (('Edward Scissorhands', ), ('Winona Ryder', )),
... ]
... '''
Create a new database with sample data, deleting the old one first, then open the database in a Python session:
$ schevo db create --app=moviereviews --delete --sample sample.db
$ schevo shell sample.db
For the doctest, let us do the equivalent:
>>> t.done()
>>> t = DocTest(schema)
>>> db = t.db
>>> db.populate()
Let us look at how we might use the relationships that Schevo has kept for us. Perhaps in the full implementation of a video store, some code would like to find all of the movies that Winona Ryder starred in. This information could be helpful for helping customers and for promotion of new movies.
Here’s how you would find those movies:
>>> actor = db.Actor.findone(name='Winona Ryder')
>>> castings = actor.m.movie_castings()
>>> movies = [casting.movie for casting in castings]
>>> for movie in movies:
... print movie
A Scanner Darkly (2006)
Edward Scissorhands (1990)
If we have a specific movie, we can find the actors in it:
>>> movie = db.Movie.findone(title='A Scanner Darkly')
>>> castings = movie.m.movie_castings()
>>> actors = [casting.actor for casting in castings]
>>> for actor in actors:
... print actor
Keanu Reeves
Winona Ryder
If we have a director, we can find the movies that he or she has directed:
>>> director = db.Director.findone(name='Richard Linklater')
>>> movies = director.m.movies()
>>> for movie in movies:
... print movie
...
A Scanner Darkly (2006)
Schevo lets you add API extensions within the x namespace. A separate namespace is used to prevent clashes between extension methods and field names.
When following the relationships above, there was some boilerplate when finding out an actor’s movies, or a movie’s actors, due to the MovieCasting extent that ties them together in a many-to-many relationship.
The developers of Schevo have found that such intermediary extents are more useful than trying to embody that type of relationship some other way. We have run into more than one instance where it became useful to add additional information about the relationship itself via extra fields in the intermediary extent.
You can use extension methods to reduce this sort of boilerplate, while still retaining the full flexibility of Schevo’s relationship model. Add a new method called x_movies to the bottom of the Actor class:
>>> schema += '''
... class Actor(E.Entity):
...
... name = f.string()
...
... _key(name)
...
... def x_movies(self):
... return [casting.movie
... for casting in self.m.movie_castings()]
... '''
Add a new method to the Movie class:
>>> schema += '''
... class Movie(E.Entity):
...
... title = f.string()
... release_date = f.date()
... director = f.entity('Director')
... description = f.string(multiline=True, required=False)
...
... _key(title)
...
... def x_actors(self):
... return [casting.actor
... for casting in self.m.movie_castings()]
...
... def __unicode__(self):
... return u'%s (%i)' % (self.title, self.release_date.year)
... '''
Close the Python session, update the database so its schema has the new methods available, then open a Python session:
$ schevo db update --app=moviereviews sample.db
$ schevo shell sample.db
For the doctest, do the equivalent:
>>> t.update(schema)
>>> db = t.db
The relationships demonstrated above are now much easier to traverse:
>>> actor = db.Actor.findone(name='Winona Ryder')
>>> for movie in actor.x.movies():
... print movie
...
A Scanner Darkly (2006)
Edward Scissorhands (1990)
>>> movie = db.Movie.findone(title='A Scanner Darkly')
>>> for actor in movie.x.actors():
... print actor
...
Keanu Reeves
Winona Ryder
Since there is a one-to-many relationship between directors and movies, the use of director.m.movies() stays the same since there is no intermediate extent used.
This tutorial has covered the following: