[TransWarp] TableDM proof of concept, request for comments
Phillip J. Eby
pje at telecommunity.com
Thu Oct 9 23:11:55 EDT 2003
At 07:39 PM 10/9/03 -0400, Phillip J. Eby wrote:
>I don't care about OQL per se. Think of it more as QBE... e.g. "find me
>all bulletins with a category whose kind is 'foo'" might be expressible as:
>Bulletin.where( category = Category.where(kind='foo') )
>Or, find me people at least 18 years old...
>Person.where( age = GE(18) )
>..who have at least one dog:
>Person.where( age = GE(18), pets = EXISTS( Pet.where(kind=PetKind.DOG) )
A further explanation of the above...
I see a future point at which model.Element classes will have a 'where'
attribute that's a "selector class". The selector class is automatically
generated from the element's type metadata. The separate class is needed
because the base element class isn't going to accept criteria in its data
fields. But, it should be possible to create an automated mapping from the
raw field info, to a criteria class.
Then, if you create an interface that represents a particular database
schema, such as 'IBulletinsForSQLite', and there are parameterized
interfaces like 'IPersistTo(IBulletinsForSQLite)', then you can define
adapters from each model class to that interface, and define adapters from
e.g. 'Bulletin.where' to 'IQueryOn(IBulletinsForSQLite)'.
So, then you could have a "database" object that was keyed by selectors, e.g.:
bulletins = bulletinsDB.find( Bulletin.where(category=someCategory) )
The argument to 'find()' is an instance of the 'Bulletin.where' class, and
the bulletinsDB adapts it to 'IQueryOn(IBulletinsForSQLite)', in order to
build the necessary SQL.
Using the adaptation framework, we could define generic adapters for simple
mappings, but still override the adapters if we wanted to create a custom
arrangement for a particular DB schema or backend.
One other piece that seems doable is to have say, a 'PARAMETER' concept, so
that you could say something like:
queryTpl = Bulletin.where( category =
query = adapt( queryTpl, IQueryOn(IBulletinsForSQLite)
results = query(kind="foo")
That is, you could generate a "compiled form" of a particular query, bound
to the database you're using. Of course, this'd probably all be done with
bindings, and preferably in such a way that the built query object can take
advantage of prepared statements (if the target database offers such a
thing). However, at the usage level that should be transparent. (But
maybe the 'buildQuery()' method would have options to control certain
aspects of that.)
Most likely, 'IQueryOn' will include methods to both build a "top-level"
query, and to add a subquery or join to an existing query. In this way, a
.where type's adapter can adapt its subparts to IQueryOn and use them to
help construct the overall query.
The net result is that a database will likely have lots and lots of query
"methods" that are pre-compiled SQL, ready to pass straight to
DBAPI... *but* which are compiled based on a DB schema and DBAPI driver
that are selected at *runtime*!
It may be that we don't have separate IQueryOn/IPersistTo adapters, since
in essence they are both simply a mapping from domain object to DB
schema. OTOH, they adapt from different types (SomeType vs.
SomeType.where). Probably, you'll do something like:
instancesProvide = [IPersistTo(someSchema)],
asAdapterForTypes = [SomeType]
instancesProvide = [IQueryOn(someSchema)],
asAdapterForTypes = [SomeType.where]
Or better yet, you'll call a simpler API that will do this for
you. Probably you'll subclass some handy base that will get the necessary
information from class attributes, e.g.:
elementType = SomeType
tableType = someSchema.SomeTable
mapping = (
And when the class is created it'll automatically declare itself as all the
right things. There are some questions in my mind, however, about how to
assemble table types. I'm assuming that they'll be defined similarly to
model.Element classes, but with only "primitive" or "structure" types
allowed for the fields.
I don't want to have to specify the schema over and over, or the protocols
to declare, so probably the TableMapping base (or whatever it's called)
should allow you to create an empty subclass for purposes of sharing, e.g.:
schema = IBulletinsForSQLite
etc. The same would probably be true for any options that these mapping
shortcut classes need for SQL generation.
Ideally, all of these mappings would be in relation to the *schema*,
without necessarily being affected by the database back-end. In practice,
though, it's hard to make a truly cross-platform schema, due to differences
in available datatypes, and even differences in allowable column names(!)
Still, it should be possible to derive one schema from another, by making
its target protocols 'protocols.Variation()'s of the original schema's
protocols, and then judiciously subclassing tables and mappings where they
need to be different. But you might need module inheritance to do it
well. :( That aspect requires some more thought.
Selectors can be used for DML queries, too. Imagine 'someDB.delete(
Bulletin.where(...) )', or 'someDB.update( Bulletin.where(...),
Bulletin.set(...) )', for example. That latter use requires a more complex
arrangement, of course.
Anyway, there's still much to think about, many holes to plug, and *many*
concepts to simplify from these rough sketches. I don't even have a
complete picture of the scope yet. One related thing I've been thinking
about is the idea that objects shouldn't have _p_oids at all, but are
instead retrieved by selectors. It's annoying right now to have to
duplicate data into the _p_oids, when most objects in business application
schemas already have "natural" primary keys as part of their requirements!
Anyway... it's late, I've gotta sleep. More on this another day.
More information about the PEAK