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]
- Create a new Schevo application called “MovieReviews”. A directory
called
MovieReviews
will be created with the new application inside. - Change into the
MovieReviews
directory. - 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]
... '''
- 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. - 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]
... '''
SchevoIcon
is an entity class. All entity classes subclass fromE.Entity
. See Schevo Namespaces to find out more about theE
namespace.
- 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. name
is a string field. See Schevo Namespaces to read more about thef
namespace.data
is an Image field.(name)
is a key for theSchevoIcon
extent. Schevo will not allow more than one entity with the same value in thename
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]
... '''
- 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
. - By default, all fields are required.
description
isn’t. (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]
db.Movie
is theMovie
extent in the database.- Extents have a
t
namespace that contains transaction methods. - By calling the
create
transaction method, we receive a transaction object that will attempt to create a new Movie entity upon execution. - Set the transaction’s fields to the values that you want the new entity to have.
date
fields are smart and will acceptdatetime.date
objects as well asYYYY-MM-DD
andMM/DD/YYYY
formatted strings.- 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)
- 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 returnsNone
. If it finds more than one, it raises aFindOneFoundMany
exception. - The
repr
representation of the entity gives you the extent name, the entity object identifier, and the entity revision. - 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