Schevo Tutorial: Movie Reviews

Installment 1

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:

  • Because of the way doctests are designed, you should be able to follow this tutorial by entering Python statements exactly as shown.
  • Typically, you won’t do this though; doctests are designed to be automatically tested for correctness, but aren’t necessarily what one would do in a real-world situation.
  • When you see code that creates a new DocTest instance (e.g. t = DocTest(schema)), you can ignore it.
  • When you see code that is adding to the schema variable (e.g. schema += ''' [some text] '''), you should just add the text in the multi-line string to your schema file.

Create a Schevo application

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]
  1. Create a new Schevo application called “MovieReviews”. A directory called MovieReviews will be created with the new application inside.
  2. Change into the MovieReviews directory.
  3. Install it into your Python environment in development mode.

The default database schema

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.

Namespace preparation

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]
...     '''
  1. The developers of Schevo tend to eschew the careless use of import *, but this one is rather useful. It imports only about 13 variables, carefully named to avoid namespace clashes with the database schema.
  2. Prepare the module’s namespace for building and storing the structure of the database schema about to be declared.

Icon storage

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]
...     '''
  1. SchevoIcon is an entity class. All entity classes subclass from E.Entity. See Schevo Namespaces to find out more about the E namespace.
  1. When the database is open, the SchevoIcon extent that is based on this class will be designated as hidden. A user interface may honor this and hide this extent from the user in some way.
  2. name is a string field. See Schevo Namespaces to read more about the f namespace.
  3. data is an Image field.
  4. (name) is a key for the SchevoIcon extent. Schevo will not allow more than one entity with the same value in the name field in this extent.

Some terminology

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.

Entity class
A class definition in a schema that describes the fields, transactions, and other characteristics that each entity of that type will have.
Extent
A collection object based on an entity class containing same-typed entities in a database. Analogous to a table in a SQL database.
Entity
An object that represents a single item in the database. Analogous to a row in a SQL database.
Field
An object that represents a property of an entity. Analogous to a column in a SQL database.

Add an entity class for movies

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]
...     '''
  1. By default, String fields will store multi-line values, but user interfaces typically render them as single-line widgets. We can hint to the user interface that it may use a multi-line input widget by setting multiline=True.
  2. By default, all fields are required. description isn’t.
  3. (title) is a key for this extent. There can be no two movies with the same title. This is a bit unrealistic, and indeed Schevo allows you to specify keys such as (title, release_date), but we’ve simplified things for this tutorial.

Create a database

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

Add some movies

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.

Transactions

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]
  1. db.Movie is the Movie extent in the database.
  2. Extents have a t namespace that contains transaction methods.
  3. By calling the create transaction method, we receive a transaction object that will attempt to create a new Movie entity upon execution.
  4. Set the transaction’s fields to the values that you want the new entity to have.
  5. date fields are smart and will accept datetime.date objects as well as YYYY-MM-DD and MM/DD/YYYY formatted strings.
  6. When the transaction object executes successfully, it returns the Movie entity that it just created.

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'

Object representations

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)
  1. The findone method of an extent finds exactly one entity in that extent whose fields match those passed to the method. If it can’t find any, it returns None. If it finds more than one, it raises a FindOneFoundMany exception.
  2. The repr representation of the entity gives you the extent name, the entity object identifier, and the entity revision.
  3. The unicode representation now gives you a more human-friendly view of things, suitable not only for interactive use but also for user interface code.

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.

Relationships

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', )),
...         ]
...     '''

Following relationships

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)

Adding your own API extensions

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.

Summary

This tutorial has covered the following:

  • Creating a skeleton for a new Schevo app
  • Adding entity classes to the schema
  • Creating and updating Schevo databases
  • Use of transaction methods and objects to create and update entities
  • Defining human-friendly summaries of entities
  • Adding sample data to a database schema
  • Defining and following relationships that Schevo maintains for you
  • Extending the API of a database using extension methods