[SQL-CVS] r860 - in trunk/SQLObject: docs sqlobject sqlobject/tests
SQLObject is a Python ORM.
Brought to you by:
ianbicking,
phd
From: <sub...@co...> - 2005-07-31 06:58:24
|
Author: ianb Date: 2005-07-31 06:58:07 +0000 (Sun, 31 Jul 2005) New Revision: 860 Added: trunk/SQLObject/docs/interface.py trunk/SQLObject/docs/test.py trunk/SQLObject/sqlobject/tests/test_auto_old.py trunk/SQLObject/sqlobject/tests/test_joins_old.py Modified: trunk/SQLObject/docs/ trunk/SQLObject/docs/SQLObject.txt trunk/SQLObject/sqlobject/col.py trunk/SQLObject/sqlobject/dbconnection.py trunk/SQLObject/sqlobject/index.py trunk/SQLObject/sqlobject/joins.py trunk/SQLObject/sqlobject/main.py trunk/SQLObject/sqlobject/sqlbuilder.py trunk/SQLObject/sqlobject/sresults.py trunk/SQLObject/sqlobject/tests/dbtest.py trunk/SQLObject/sqlobject/tests/test_auto.py trunk/SQLObject/sqlobject/tests/test_enum.py trunk/SQLObject/sqlobject/tests/test_inheritance.py trunk/SQLObject/sqlobject/tests/test_joins.py trunk/SQLObject/sqlobject/tests/test_joins_conditional.py trunk/SQLObject/sqlobject/tests/test_select.py trunk/SQLObject/sqlobject/tests/test_slice.py trunk/SQLObject/sqlobject/tests/test_style.py trunk/SQLObject/sqlobject/tests/test_style_old.py trunk/SQLObject/sqlobject/tests/test_unicode.py Log: Major refactoring to move soClass._columns and company into sqlmeta. Property changes on: trunk/SQLObject/docs ___________________________________________________________________ Name: svn:ignore - *.html + *.html data.db Modified: trunk/SQLObject/docs/SQLObject.txt =================================================================== --- trunk/SQLObject/docs/SQLObject.txt 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/docs/SQLObject.txt 2005-07-31 06:58:07 UTC (rev 860) @@ -4,22 +4,712 @@ .. contents:: Contents: -.. include:: SQLObjectIntro.txt -.. include:: SQLObjectRequirements.txt -.. include:: SQLObjectComparison.txt -.. include:: SQLObjectFuture.txt +Author, Site, and License +========================= +SQLObject is by Ian Bicking (ia...@co...). The website is +sqlobject.org__. + +__ http://sqlobject.org + +The code is licensed under the `Lesser General Public License`_ +(LGPL). + +.. _`Lesser General Public License`: http://www.gnu.org/copyleft/lesser.html + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +Introduction +============ + +SQLObject is an *object-relational mapper*. It allows you to +translate RDBMS table rows into Python objects, and manipulate those +objects to transparently manipulate the database. + +In using SQLObject, you will create a class definition that will +describe how the object translates to the database table. SQLObject +will produce the code to access the database, and update the database +with your changes. The generated interface looks similar to any other +interface, and callers need not be aware of the database backend. + +SQLObject also includes a novel feature to avoid generating, +textually, your SQL queries. This also allows non-SQL databases to be +used with the same query syntax. + +Requirements +============ + +Currently SQLObject supports MySQL_, PostgreSQL_ (via ``psycopg``), +SQLite_, Firebird_, Sybase_, and `MAX DB`_ (also known as SAP DB). + +.. _PostgreSQL: http://postgresql.org +.. _SQLite: http://sqlite.org +.. _Firebird: http://firebird.sourceforge.net + +Python 2.2 or higher is required. SQLObject makes extensive use of +new-style classes. + +Compared To Other Database Wrappers +=================================== + +There are several object-relational mappers (ORM) for Python. I +honestly can't comment deeply on the quality of those packages, but +I'll try to place SQLObject in perspective. + +SQLObject uses new-style classes extensively. The resultant objects +have a new-style feel as a result -- setting attributes has side +effects (it changes the database), and defining classes has side +effects (through the use of metaclasses). Attributes are generally +exposed, not marked private, knowing that they can be made dynamic +or write-only later. + +SQLObject creates objects that feel similar to normal Python objects +(with the semantics of new-style classes). An attribute attached to a +column doesn't look different than an attribute that's attached to a +file, or an attribute that is calculated. It is a specific goal that +you be able to change the database without changing the interface, +including changing the scope of the database, making it more or less +prominent as a storage mechanism. + +This is in contrast to some ORMs that provide a dictionary-like +interface to the database (for example, PyDO_). The dictionary +interface distinguishes the row from a normal Python object. I also +don't care for the use of strings where an attribute seems more +natural -- columns are limited in number and predefined, just like +attributes. (Note: newer version of PyDO apparently allow attribute +access as well) + +.. _PyDO: http://skunkweb.sourceforge.net/pydo.html + +SQLObject is, to my knowledge, unique in using metaclasses to +facilitate this seemless integration. Some other ORMs use code +generation to create an interface, expressing the schema in a CSV or +XML file (for example, MiddleKit, part of Webware_). By using +metaclasses you are able to comfortably define your schema in the +Python source code. No code generation, no weird tools, no +compilation step. + +.. _Webware: http://webware.sourceforge.net + +SQLObject provides a strong database abstraction, allowing +cross-database compatibility (so long as you don't sidestep +SQLObject). + +SQLObject has joins, one-to-many, and many-to-many, something which +many ORMs do not have. The join system is also intended to be +extensible. + +You can map between database names and Python attribute and class +names; often these two won't match, or the database style would be +inappropriate for a Python attribute. This way your database schema +does not have to be designed with SQLObject in mind, and the resulting +classes do not have to inherit the database's naming schemes. + +Future +====== + +Here are some things I plan: + +* More databases supported. There has been interest and some work in + the progress for Oracle, Sybase, and MS-SQL support. +* Better transaction support -- right now you can use transactions + for the database, but the object isn't transaction-aware, so + non-database persistence won't be able to be rolled back. +* Optimistic locking and other techniques to handle concurrency. +* Profile of SQLObject performance, so that I can identify bottlenecks. +* Increase hooks with FormEncode (unreleased) validation and form + generation package, so SQLObject classes (read: schemas) can be + published for editing more directly and easily. +* Automatic joins in select queries. +* More kinds of joins, and more powerful join results (closer to how + `select` works). + +See also the `Plan for 0.6`__. + +.. __: Plan06.html + Using SQLObject: An Introduction ================================ -Let's start off quickly... +Let's start off quickly. We'll generally just import everything from +the ``sqlobject`` class:: -.. include:: SQLObjectDeclaration.txt -.. include:: SQLObjectUse.txt -.. include:: SQLObjectLazy.txt -.. include:: SQLObjectOneToMany.txt -.. include:: SQLObjectManyToMany.txt -.. include:: SQLObjectSelect.txt + >>> from sqlobject import * + >>> import sys, os + +Declaring the Class +------------------- + +Lets first set up a connection:: + + >>> db_filename = os.path.abspath('data.db') + >>> if os.path.exists(db_filename): + ... os.unlink(db_filename) + >>> sqlhub.processConnection = connectionForURI( + ... 'sqlite:' + db_filename) + +We'll develop a simple addressbook-like database. We could create the +tables ourselves, and just have SQLObject access those tables, but for +now we'll let SQLObject do that work. First, the class: + + >>> class Person(SQLObject): + ... + ... firstName = StringCol() + ... middleInitial = StringCol(length=1, default=None) + ... lastName = StringCol() + +Many basic table schemas won't be any more complicated than that. +`firstName`, `middleInitial`, and `lastName` are all columns in the +database. The general schema implied by this class definition is:: + + CREATE TABLE person ( + id INT PRIMARY KEY AUTO_INCREMENT, + first_name TEXT, + middle_initial CHAR(1), + last_name TEXT + ); + +This is for MySQL. The schema for other databases looks slightly +different (especially the ``id`` column). You'll notice the names +were changed from mixedCase to underscore_separated -- this is done by +the `style object`_. There are a variety of ways to handle that names +that don't fit conventions (see `Irregular Naming`_). + +.. _`style object`: `Changing the Naming Style`_ + +The tables don't yet exist. We'll let SQLObject create them:: + + >>> Person.createTable() + +We can change the type of the various columns by using something other +than `StringCol`, or using different arguments. More about this in +`Subclasses of Col`_. + +If you don't want to do table creation (you already have tables, or +you want to create the tables yourself), you can just use the vague +`Col` class. SQLObject doesn't do much type checking, allowing the +database and the adapter to handle most of the type conversion. +Databases generally do their own type coercion on inputs. + +You'll note that the ``id`` column is not given in the class +definition, it is implied. For MySQL databases it should be defined +as ``INT PRIMARY KEY AUTO_INCREMENT``, in Postgres ``SERIAL PRIMARY +KEY``, and in SQLite as ``INTEGER PRIMARY KEY``. You can `override +the name`__, but some immutable primary key must exist (`you can use +non-integer keys`_ with some extra effort). + +__ idName_ +.. _`you can use non-integer keys`: `Non-Integer Keys`_ + +Using the Class +--------------- + +Now that you have a class, how will you use it? We'll be considering +the class defined above. + +To create a new object (and row), use class instantiation, like:: + + >>> Person(firstName="John", lastName="Doe") + <Person 1 lastName='Doe' middleInitial=None firstName='John'> + +If you had left out ``firstName`` or ``lastName`` you would have +gotten an error, as no default was given for these columns +(``middleInitial`` has a default, so it will be set to ``NULL``, the +SQL equivalent of ``None``). + +.. note:: + + In SQLObject NULL/None does *not* mean default. NULL is a funny + thing; it mean very different things in different contexts and to + different people. Sometimes it means "default", sometimes "not + applicable", sometimes "unknown". If you want a default, NULL or + otherwise, you always have to be explicit in your class + definition. + + Also note that the SQLObject default isn't the same as the + database's default (SQLObject never uses the database's default). + +You can use the class method `.get()` to fetch instances that +already exist:: + + >>> Person.get(1) + <Person 1 lastName='Doe' middleInitial=None firstName='John'> + +When you create an object, it is immediately inserted into the +database. SQLObject generally uses the database as immediate storage, +unlike some other systems where you explicitly save objects into a +database. + +Here's a longer example of using the class:: + + >>> p = Person.get(1) + >>> p + <Person 1 lastName='Doe' middleInitial=None firstName='John'> + >>> p.firstName + 'John' + >>> p.middleInitial = 'Q' + >>> p.middleInitial + 'Q' + >>> p2 = Person.get(1) + >>> p2 + <Person 1 lastName='Doe' middleInitial='Q' firstName='John'> + >>> p is p2 + True + +Columns are accessed like attributes. (This uses the ``property`` +feature of Python 2.2, so that retrieving and setting these attributes +executes code). Also note that objects are unique -- there is +generally only one ``Person`` instance of a particular id in memory at +any one time. If you ask for a person by a particular ID more than +once, you'll get back the same instance. This way you can be sure of +a certain amount of consistency if you have multiple threads accessing +the same data (though of course across processes there can be no +sharing of an instance). This isn't true if you're using +transactions_, which are necessarily isolated. + +To get an idea of what's happening behind the surface, I'll give the +same actions with the SQL that is sent, along with some commentary:: + + >>> # This will make SQLObject print out the SQL it executes: + >>> Person._connection.debug = True + >>> p = Person(firstName='Bob', lastName='Hope') + 1/QueryIns: INSERT INTO person (last_name, middle_initial, first_name) VALUES ('Hope', NULL, 'Bob') + 1/COMMIT : auto + 1/QueryOne: SELECT last_name, middle_initial, first_name FROM person WHERE id = 2 + 1/COMMIT : auto + >>> p + <Person 2 lastName='Hope' middleInitial=None firstName='Bob'> + >>> p.middleInitial = 'Q' + 1/Query : UPDATE person SET middle_initial = 'Q' WHERE id = 2 + 1/COMMIT : auto + >>> p2 = Person.get(1) + >>> # Note: no database access, since we're just grabbing the same + >>> # instance we already had. + +Hopefully you see that the SQL that gets sent is pretty clear and +predictable. To view the SQL being sent, add ``?debug=t`` to your +connection URI, or set the ``debug`` attribute on the connection, and +all SQL will be printed to the console. This can be reassuring, and I +would encourage you to try it. + +.. comment: + + >>> Person._connection.debug = False + +As a small optimization, instead of assigning each attribute +individually, you can assign a number of them using the ``set`` +method, like:: + + >>> p.set(firstName='Robert', lastName='Hope Jr.') + +This will send only one ``UPDATE`` statement. You can also use `set` +with non-database properties (there's no benefit, but it helps hide +the difference between database and non-database attributes). + +Lazy Updates +------------ + +By default SQLObject sends an ``UPDATE`` to the database for every +attribute you set, or everytime you call ``.set()``. If you want to +avoid this many updates, add ``_lazyUpdate = True`` to your class +definition. Then updates will only be written to the database when +you call ``inst.syncUpdate()`` or ``obj.sync()``: ``.sync()`` also +refetches the data from the database, which ``.syncUpdate()`` does not +do. + +When enabled instances will have a property ``dirty``, which indicates +if there are pending updates. Inserts are still done immediately. + +One-to-Many Relationships +------------------------- + +A real address book should have people, but also addresses. + +First, let's define the new address table. People can have multiple +addresses, of course:: + + >>> class Address(SQLObject): + ... + ... street = StringCol() + ... city = StringCol() + ... state = StringCol(length=2) + ... zip = StringCol(length=9) + ... person = ForeignKey('Person') + >>> Address.createTable() + +Note the column ``person = ForeignKey("Person")``. This is a +reference to a `Person` object. We refer to other classes by name +(with a string) to avoid circular dependencies. In the database +there will be a ``person_id`` column, type ``INT``, which points to +the ``person`` column. + +We want an attribute that gives the addresses for a person. In a +class definition we'd do:: + + class Person(SQLObject): + ... + addresses = MultipleJoin('Address') + +But we already have the class. We can add this to the class +in-place:: + + >>> Person.addJoin(MultipleJoin('Address', + ... joinMethodName='addresses')) + +.. note:: + + In almost all cases you can modify SQLObject classes after they've + been created. Having attributes like ``*Col`` objects is + equivalent to calling certain class methods (like + ``addColumn()``). + +Now we can get the backreference with ``aPerson.addresses``, which +returns a list. An example:: + + >>> p.addresses + [] + >>> Address(street='123 W Main St', city='Smallsville', + ... state='MN', zip='55407', person=p) + <Address 1 ...> + >>> p.addresses + [<Address 1 ...>] + +Many-to-Many Relationships +-------------------------- + +For this example we will have user and role objects. The two have a +many-to-many relationship, which is represented with the +`RelatedJoin`. + + >>> class User(SQLObject): + ... + ... class sqlmeta: + ... # user is a reserved word in some databases, so we won't + ... # use that for the table name: + ... table = "user_table" + ... + ... username = StringCol(alternateID=True, length=20) + ... # We'd probably define more attributes, but we'll leave + ... # that excersize to the reader... + ... + ... roles = RelatedJoin('Role') + + >>> class Role(SQLObject): + ... + ... name = StringCol(alternateID=True, length=20) + ... + ... users = RelatedJoin('User') + + >>> User.createTable() + >>> Role.createTable() + +Note the use of the ``sqlmeta`` class. This class is used to store +different kinds of metadata (and override that metadata, like +``table``). This is new in SQLObject 0.7. + +And usage:: + + >>> bob = User(username='bob') + >>> tim = User(username='tim') + >>> jay = User(username='jay') + >>> admin = Role(name='admin') + >>> editor = Role(name='editor') + >>> bob.addRole(admin) + >>> bob.addRole(editor) + >>> tim.addRole(editor) + >>> bob.roles + [<Role 1 name='admin'>, <Role 2 name='editor'>] + >>> tim.roles + [<Role 2 name='editor'>] + >>> jay.roles + [] + >>> admin.users + [<User 1 username='bob'>] + >>> editor.users + [<User 1 username='bob'>, <User 2 username='tim'>] + +In the process an intermediate table is created, ``role_user``, which +references both of the other classes. This table is never exposed as +a class, and its rows do not have equivalent Python objects -- this +hides some of the nuisance of a many-to-many relationship. + +You may notice that the columns have the extra keyword argument +`alternateID`. If you use ``alternateID=True``, this means that the +column uniquely identifies rows -- like a username uniquely identifies +a user. This identifier is in addition to the primary key (``id``), +which is always present. + +.. note:: + + SQLObject has a strong requirement that the primary key be unique + and *immutable*. You cannot change the primary key through + SQLObject, and if you change it through another mechanism you can + cause inconsistency in any running SQLObject program (and in your + data). For this reason meaningless integer IDs are encouraged -- + something like a username that could change in the future may + uniquely identify a row, but it may be changed in the future. So + long as it is not used to reference the row, it is also *safe* to + change it in the future. + +A alternateID column creates a class method, like ``byUsername`` for a +column named ``username`` (or you can use the `alternateMethodName` +keyword argument to override this). Its use: + + >>> User.byUsername('bob') + <User 1 username='bob'> + >>> Role.byName('admin') + <Role 1 name='admin'> + +Selecting Multiple Objects +-------------------------- + +While the full power of all the kinds of joins you can do with a +relational database are not revealed in SQLObject, a simple ``SELECT`` +is available. + +``select`` is a class method, and you call it like (with the SQL +that's generated):: + + >>> Person._connection.debug = True + >>> peeps = Person.select(Person.q.firstName=="John") + >>> list(peeps) + SELECT person.id FROM person WHERE person.first_name = 'John'; + [<Person 1 lastName='Doe' middleInitial=None firstName='John'>] + +This example returns everyone with the first name John. An expression +could be more complicated as well, like:: + + >>> peeps = Person.select( + ... AND(Address.q.personID == Person.q.id, + ... Address.q.zip.startswith('504'))) + >>> list(peeps) + SELECT person.id FROM person, address WHERE (address.person_id = person.id AND address.zip LIKE '612%'); + [] + +You'll note that classes have an attribute ``q``, which gives access +to special objects for constructing query clauses. All attributes +under ``q`` refer to column names and if you construct logical +statements with these it'll give you the SQL for that statement. You +can also create your SQL more manually:: + + >>> peeps = Person.select("""address.id = person.id AND + ... address.zip LIKE '504%'""", + ... clauseTables=['address']) + +Note that you have to use ``clauseTables`` if you use tables besides +the one you are selecting from. If you use the ``q`` attributes +SQLObject will automatically figure out what extra classes you might +have used. + +You should use `MyClass.sqlrepr` to quote any values you use if you +create SQL manually (quoting is automatic if you use ``q``). + +.. _orderBy: + +You can use the keyword arguments `orderBy` to create ``ORDER BY`` in +the select statements: `orderBy` takes a string, which should be the +*database* name of the column, or a column in the form +``Person.q.firstName``. You can use ``"-colname"`` to specify +descending order, or call ``MyClass.select().reversed()``. + +You can use the special class variable `_defaultOrder` to give a +default ordering for all selects. To get an unordered result when +`_defaultOrder` is used, use ``orderBy=None``. + +Select results are generators, which are lazily evaluated. So the SQL +is only executed when you iterate over the select results, or if you +use ``list()`` to force the result to be executed. When you iterate +over the select results, rows are fetched one at a time. This way you +can iterate over large results without keeping the entire result set +in memory. You can also do things like ``.reversed()`` without +fetching and reversing the entire result -- instead, SQLObject can +change the SQL that is sent so you get equivalent results. + +You can also slice select results. This modifies the SQL query, so +``peeps[:10]`` will result in ``LIMIT 10`` being added to the end of +the SQL query. If the slice cannot be performed in the SQL (e.g., +peeps[:-10]), then the select is executed, and the slice is performed +on the list of results. This will generally only happen when you use +negative indexes. + +In certain cases, you may get a select result with an object in it +more than once, e.g., in some joins. If you don't want this, you can +add the keyword argument ``MyClass.select(..., distinct=True)``, which +results in a ``SELECT DISTINCT`` call. + +You can get the length of the result without fetching all the results +by calling ``count`` on the result object, like +``MyClass.select().count()``. A ``COUNT(*)`` query is used -- the +actual objects are not fetched from the database. Together with +slicing, this makes batched queries easy to write: + + start = 20 + size = 10 + query = Table.select() + results = query[start:start+size] + total = query.count() + print "Showing page %i of %i" % (start/size + 1, total/size + 1) + +.. note:: + + There are several factors when considering the efficiency of this + kind of batching, and it depends very much how the batching is + being used. Consider a web application where you are showing an + average of 100 results, 10 at a time, and the results are ordered + by the date they were added to the database. While slicing will + keep the database from returning all the results (and so save some + communication time), the database will still have to scan through + the entire result set to sort the items (so it knows which the + first ten are), and depending on your query may need to scan + through the entire table (depending on your use of indexes). + Indexes are probably the most important way to improve importance + in a case like this, and you may find caching to be more effective + than slicing. + + In this case, caching would mean retrieving the *complete* results. + You can use ``list(MyClass.select(...))`` to do this. You can save + these results for some limited period of time, as the user looks + through the results page by page. This means the first page in a + search result will be slightly more expensive, but all later pages + will be very cheap. + +For more information on the where clause in the queries, see the +`SQLBuilder documentation`_. + +.. _`SQLBuilder documentation`: SQLBuilder.html + +Select-By Method +~~~~~~~~~~~~~~~~ + +An alternative to ``.select`` is ``.selectBy``. It works like: + + >>> peeps = Person.selectBy(firstName="John", lastName="Doe") + +Each keyword argument is a column, and all the keyword arguments +are ANDed together. The return value is a `SelectResult`, so you +can slice it, count it, order it, etc. + +Customizing the Objects +----------------------- + +While we haven't done so in the examples, you can include your own +methods in the class definition. Writing your own methods should be +obvious enough (just do so like in any other class), but there are +some other details to be aware of. + +Initializing the Objects +~~~~~~~~~~~~~~~~~~~~~~~~ + +There are two ways SQLObject instances can come into existance: they +can be fetched from the database, or they can be inserted into the +database. In both cases a new Python object is created. This makes +the role of `__init__` a little confusing. + +In general, you should not touch `__init__`. Instead use the `_init` +method, which is called after an object is fetched or inserted. This +method has the signature ``_init(self, id, connection=None, +selectResults=None)``, though you may just want to use ``_init(self, +*args, **kw)``. **Note:** don't forget to call +``SQLObject._init(self, *args, **kw)`` if you override the method! + +Adding Magic Attributes (properties) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use all the normal techniques for defining methods in this +new-style class, including `classmethod`, `staticmethod`, and +`property`, but you can also use a shortcut. If you have a method +that's name starts with ``_set_``, ``_get_``, ``_del_``, or ``_doc_``, +it will be used to create a property. So, for instance, say you have +images stored under the ID of the person in the ``/var/people/images`` +directory: + + class Person(SQLObject): + # ... + + def imageFilename(self): + return 'images/person-%s.jpg' % self.id + + def _get_image(self): + if not os.path.exists(self.imageFilename()): + return None + f = open(self.imageFilename()) + v = f.read() + f.close() + return v + + def _set_image(self, value): + # assume we get a string for the image + f = open(self.imageFilename(), 'w') + f.write(value) + f.close() + + def _del_image(self, value): + # I usually wouldn't include a method like this, but for + # instructional purposes... + os.unlink(self.imageFilename()) + + +Later, you can use the ``.image`` property just like an attribute, and +the changes will be reflected in the filesystem by calling these +methods. This is a good technique for information that is better to +keep in files as opposed to the database (such as large, opaque data +like images). + +You can also pass an ``image`` keyword argument to the constructor +or the `set` method, like ``Person(..., image=imageText)``. + +All of the methods (``_get_``, ``_set_``, etc) are optional -- you can +use any one of them without using the others. So you could define +just a ``_get_attr`` method so that ``attr`` was read-only. + +Overriding Column Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's a little more complicated if you want to override the behavior of +an database column attribute. For instance, imagine there's special +code you want to run whenever someone's name changes. In many systems +you'd do some custom code, then call the superclass's code. But the +superclass (``SQLObject``) doesn't know anything about the column in +your subclass. It's even worse with properties. + +SQLObject creates methods like ``_set_lastName`` for each of your +columns, but again you can't use this, since there's no superclass to +reference (and you can't write ``SQLObject._set_lastName(...)``, +because the SQLObject class doesn't know about your class's columns). +You want to override that ``_set_lastName`` method yourself. + +To deal with this, SQLObject creates two methods for each getter and +setter, for example: ``_set_lastName`` and ``_SO_set_lastName``. So +to intercept all changes to ``lastName``: + + class Person(SQLObject): + lastName = StringCol() + firstName = StringCol() + + def _set_lastName(self, value): + self.notifyLastNameChange(value) + self._SO_set_lastName(value) + +Or perhaps you want to constrain a phone numbers to be actual +digits, and of proper length, and make the formatting nice: + +.. raw:: html + :file: ../examples/snippets/phonenumber_magicoverride.html + +.. note:: + + You should be a little cautious when modifying data that gets set + in an attribute. Generally someone using your class will expect + that the value they set the attribute to will be the same value + they get back. In this example we removed some of the characters + before putting it in the database, and reformatted it on the way + out. One advantage of methods (as opposed to attribute access) is + that the programmer is more likely to expect this disconnect. + + .. include:: SQLObjectCustomization.txt Reference Added: trunk/SQLObject/docs/interface.py =================================================================== --- trunk/SQLObject/docs/interface.py 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/docs/interface.py 2005-07-31 06:58:07 UTC (rev 860) @@ -0,0 +1,355 @@ +""" +This is a not-very-formal outline of the interface that SQLObject +provides. While its in the form of a formal interface, it doesn't +use any interface system. +""" + +class Interface(object): + pass + +class ISQLObject(Interface): + + sqlmeta = """ + A class or instance representing internal state and methods for + introspecting this class. + + ``MyClass.sqlmeta`` is a class, and ``myInstance.sqlmeta`` is an + instance of this class. So every instance gets its own instance + of the metadata. + + This object follows the ``Isqlmeta`` interface. + """ + + # classmethod + def get(id, connection=None): + """ + Returns the object with the given `id`. If `connection` is + given, then get the object from the given connection + (otherwise use the default or configured connection) + + It raises ``SQLObjectNotFound`` if no row exists with that ID. + """ + + # classmethod + def selectBy(connection=None, **attrs): + """ + Performs a ``SELECT`` in the given `connection` (or default + connection). + + Each of the keyword arguments should be a column, and the + equality comparisons will be ``ANDed`` together to produce the + result. + """ + + # classmethod + def dropTable(ifExists=False, dropJoinTables=True, cascade=False, + connection=None): + """ + Drops this table from the database. If ``ifExists`` is true, + then it is not an error if the table doesn't exist. + + Join tables (mapping tables for many-to-many joins) are + dropped if this class comes alphabetically before the other + join class, and if ``dropJoinTables`` is true. + + ``cascade`` is passed to the connection, and if true tries to + drop tables that depend on this table. + """ + + # classmethod + def createTable(ifNotExists=False, createJoinTables=True, + createIndexes=True, connection=None): + """ + Creates the table. If ``ifNotExists`` is true, then it is not + an error if the table already exists. + + Join tables (mapping tables for many-to-many joins) are + created if this class comes alphabetically before the other + join class, and if ``createJoinTables`` is true. + + If ``createIndexes`` is true, indexes are also created. + """ + + # classmethod + def createTableSQL(createJoinTables=True, connection=None, + createIndexes=True): + """ + Returns the SQL that would be sent with the analogous call + to ``Class.createTable(...)`` + """ + + def sync(): + """ + This will refetch the data from the database, putting it in + sync with the database (in case another process has modified + the database since this object was fetched). It will raise + ``SQLObjectNotFound`` if the row has been deleted. + + This will call ``self.syncUpdate()`` if ``lazyUpdates`` are + on. + """ + + def syncUpdate(): + """ + If ``.sqlmeta.lazyUpdates`` is true, then this method must be + called to push accumulated updates to the server. + """ + + def expire(): + """ + This will remove all the column information from the object. + The next time this information is used, a ``SELECT`` will be + made to re-fetch the data. This is like a lazy ``.sync()``. + """ + + def set(**attrs): + """ + This sets many column attributes at once. ``obj.set(a=1, + b=2)`` is equivalent to ``obj.a=1; obj.b=2``, except that it + will be grouped into one ``UPDATE`` + """ + + def destroySelf(): + """ + Deletes this row from the database. This is called on + instances, not on the class. The object still persists, + because objects cannot be deleted from the Python process + (they can only be forgotten about, at which time they are + garbage collected). The object becomes obsolete, and further + activity on it will raise errors. + """ + + def sqlrepr(obj, connection=None): + """ + Returns the SQL representation of the given object, for the + configured database connection. + """ + +class Isqlmeta(Interface): + + table = """ + The name of the table in the database. This is derived from + ``style`` and the class name if no explicit name is given. + """ + + idName = """ + The name of the primary key column in the database. This is + derived from ``style`` if no explicit name is given. + """ + + idType = """ + A function that coerces/normalizes IDs when setting IDs. This + is ``int`` by default (all IDs are normalized to integers). + """ + + style = """ + An instance of the ``IStyle`` interface. This maps Python + identifiers to database names. + """ + + lazyUpdate = """ + A boolean (default false). If true, then setting attributes on + instances (or using ``inst.set(...)`` will not send ``UPDATE`` + queries immediately (you must call ``inst.syncUpdates()`` or + ``inst.sync()`` first). + """ + + defaultOrder = """ + When selecting objects and not giving an explicit order, this + attribute indicates the default ordering. It is like this value + is passed to ``.select()`` and related methods; see those method's + documentation for details. + """ + + cacheValues = """ + A boolean (default true). If true, then the values in the row are + cached as long as the instance is kept (and ``inst.expire()`` is + not called). If false, then every attribute access causes a + ``SELECT`` (which is absurdly inefficient). + """ + + registry = """ + Because SQLObject uses strings to relate classes, and these + strings do not respect module names, name clashes will occur if + you put different systems together. This string value serves + as a namespace for classes. + """ + + fromDatabase = """ + A boolean (default false). If true, then on class creation the + database will be queried for the table's columns, and any missing + columns (possible all columns) will be added automatically. + """ + + columns = """ + A dictionary of ``{columnName: anSOColInstance}``. You can get + information on the columns via this read-only attribute. + """ + + columnList = """ + A list of the values in ``columns``. Sometimes a stable, ordered + version of the columns is necessary; this is used for that. + """ + + columnDefinitions = """ + A dictionary like ``columns``, but contains the original column + definitions (which are not class-specific, and have no logic). + """ + + joins = """ + A list of all the Join objects for this class. + """ + + indexes = """ + A list of all the indexes for this class. + """ + + # Instance attributes + + expired = """ + A boolean. If true, then the next time this object's column + attributes are accessed a query will be run. + """ + + # Methods + + def addColumn(columnDef, changeSchema=False, connection=None): + """ + Adds the described column to the table. If ``changeSchema`` + is true, then an ``ALTER TABLE`` query is called to change the + database. + + Attributes given in the body of the SQLObject subclass are + collected and become calls to this method. + """ + + def delColumn(column, changeSchema=False, connection=None): + """ + Removes the given column (either the definition from + ``columnDefinition`` or the SOCol object from ``columns``). + + If ``changeSchema`` is true, then an ``ALTER TABLE`` query is + made. + """ + + def addColumnsFromDatabase(connection=None): + """ + Adds all the columns from the database that are not already + defined. If the ``fromDatabase`` attribute is true, then + this is called on class instantiation. + """ + + def addJoin(joinDef): + """ + Adds a join to the class. + """ + + def delJoin(joinDef): + """ + Removes a join from the class. + """ + + def addIndex(indexDef): + """ + Adds the index to the class. + """ + + +class ICol(Interface): + + def __init__(name=None, **kw): + """ + Creates a column definition. This is an object that describes + a column, basically just holding the keywords for later + creating an ``SOCol`` (or subclass) instance. Subclasses of + ``Col`` (which implement this interface) typically create the + related subclass of ``SOCol``. + """ + + name = """ + The name of the column. If this is not given in the constructor, + ``SQLObject`` will set this attribute from the variable name this + object is assigned to. + """ + +class ISOCol(Interface): + + """ + This is a column description that is bound to a single class. + This cannot be shared by subclasses, so a new instance is created + for every new class (in cases where classes share columns). + + These objects are created by ``Col`` instances, you do not create + them directly. + """ + + name = """ + The name of the attribute that points to this column. This is the + Python name of the column. + """ + + columnDef = """ + The ``Col`` object that created this instance. + """ + + immutable = """ + Boolean, default false. If true, then this column cannot be + modified. It cannot even be modified at construction, rendering + the table read-only. This will probably change in the future, as + it renders the option rather useless. + """ + + cascade = """ + If a foreign key, then this indicates if deletes in that foreign + table should cascade into this table. This can be true (deletes + cascade), false (the default, they do not cascade), or ``'null'`` + (this column is set to ``NULL`` if the foreign key is deleted). + """ + + constraints = """ + A list of ... @@? + """ + + notNone = """ + Boolean, default false. It true, then ``None`` (aka ``NULL``) is + not allowed in this column. Also the ``notNull`` attribute can be + used. + """ + + foreignKey = """ + If not None, then this column points to another table. The + attribute is the name (a string) of that table/class. + """ + + dbName = """ + The name of this column in the database. + """ + + alternateID = """ + Boolean, default false. If true, then this column is assumed to + be unique, and you can fetch individual rows based on this + column's value. This will add a method ``byAttributeName`` to the + parent SQLObject subclass. + """ + + unique = """ + Boolean, default false. If this column is unique; effects the + database ``CREATE`` statement, and is implied by + ``alternateID=True``. + """ + + validator = """ + A IValidator object. All setting of this column goes through the + ``fromPython`` method of the validator. All getting of this + column from the database goes through ``toPython``. + """ + + default = """ + A value that holds the default value for this column. If the + default value passed in is a callable, then that value is called + to return a default (a typical example being ``DateTime.now``). + """ + + sqlType = """ + The SQL type of the column, overriding the default column type. + """ Added: trunk/SQLObject/docs/test.py =================================================================== --- trunk/SQLObject/docs/test.py 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/docs/test.py 2005-07-31 06:58:07 UTC (rev 860) @@ -0,0 +1,15 @@ +import sys, os + +if sys.version_info >= (2, 4): + import doctest +else: + raise ImportError("Python 2.4 doctest required") + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +def test(): + for doc in ['SQLObject.txt']: + doctest.testfile(doc, optionflags=doctest.ELLIPSIS) + +if __name__ == '__main__': + test() Modified: trunk/SQLObject/sqlobject/col.py =================================================================== --- trunk/SQLObject/sqlobject/col.py 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/sqlobject/col.py 2005-07-31 06:58:07 UTC (rev 860) @@ -363,7 +363,9 @@ self.kw = kw def _set_name(self, value): - assert self._name is None, "You cannot change a name after it has already been set (from %s to %s)" % (self.name, value) + assert self._name is None or self._name == value, ( + "You cannot change a name after it has already been set " + "(from %s to %s)" % (self.name, value)) self._name = value def _get_name(self): @@ -374,7 +376,13 @@ def withClass(self, soClass): return self.baseClass(soClass=soClass, name=self._name, **self.kw) + def __repr__(self): + return '<%s %s %s>' % ( + self.__class__.__name__, hex(abs(id(self)))[2:], + self._name or '(unnamed)') + + class SOStringLikeCol(SOCol): """A common ancestor for SOStringCol and SOUnicodeCol""" def __init__(self, **kw): Modified: trunk/SQLObject/sqlobject/dbconnection.py =================================================================== --- trunk/SQLObject/sqlobject/dbconnection.py 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/sqlobject/dbconnection.py 2005-07-31 06:58:07 UTC (rev 860) @@ -290,6 +290,7 @@ print '%(n)2i%(threadName)s/%(name)s%(spaces)s%(sep)s %(s)s' % locals() def _executeRetry(self, conn, cursor, query): + print query return cursor.execute(query) def _query(self, conn, s): @@ -398,7 +399,7 @@ ", ".join(tables)) else: columns = ", ".join(["%s.%s" % (cls.sqlmeta.table, col.dbName) - for col in cls.sqlmeta._columns]) + for col in cls.sqlmeta.columnList]) if columns: q += "%s.%s, %s FROM %s" % \ (cls.sqlmeta.table, cls.sqlmeta.idName, columns, @@ -465,8 +466,8 @@ else: desc = False assert sqlbuilder.sqlIdentifier(s), "Strings in clauses are expected to be column identifiers. I got: %r" % s - if select.sourceClass.sqlmeta._columnDict.has_key(s): - s = select.sourceClass.sqlmeta._columnDict[s].dbName + if s in select.sourceClass.sqlmeta.columns: + s = select.sourceClass.sqlmeta.columns[s].dbName if desc: return sqlbuilder.DESC(sqlbuilder.SQLConstant(s)) else: @@ -520,7 +521,7 @@ def createColumns(self, soClass): columnDefs = [self.createIDColumn(soClass)] \ + [self.createColumn(soClass, col) - for col in soClass.sqlmeta._columns] + for col in soClass.sqlmeta.columnList] return ",\n".join([" %s" % c for c in columnDefs]) def createColumn(self, soClass, col): @@ -624,7 +625,7 @@ if 'id' in kw: data[soClass.sqlmeta.idName] = kw['id'] else: - for key, col in soClass.sqlmeta._columnDict.items(): + for key, col in soClass.sqlmeta.columns.items(): if key in kw: data[col.dbName] = kw[key] elif col.foreignName in kw: Modified: trunk/SQLObject/sqlobject/index.py =================================================================== --- trunk/SQLObject/sqlobject/index.py 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/sqlobject/index.py 2005-07-31 06:58:07 UTC (rev 860) @@ -37,7 +37,7 @@ columnName = desc['column'] if not isinstance(columnName, str): columnName = columnName.name - colDict = self.soClass.sqlmeta._columnDict + colDict = self.soClass.sqlmeta.columns if not colDict.has_key(columnName): for possible in colDict.values(): if possible.origName == columnName: @@ -136,4 +136,10 @@ def withClass(self, soClass): return self.baseClass(soClass=soClass, **self.kw) + def __repr__(self): + return '<%s %s %s>' % ( + self.__class__.__name__, + hex(abs(id(self)))[2:], + self.kw) + __all__ = ['DatabaseIndex'] Modified: trunk/SQLObject/sqlobject/joins.py =================================================================== --- trunk/SQLObject/sqlobject/joins.py 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/sqlobject/joins.py 2005-07-31 06:58:07 UTC (rev 860) @@ -16,7 +16,6 @@ def __init__(self, otherClass=None, **kw): kw['otherClass'] = otherClass - kw['joinDef'] = self self.kw = kw if self.kw.has_key('joinMethodName'): self._joinMethodName = popKey(self.kw, 'joinMethodName') @@ -37,6 +36,7 @@ self._joinMethodName = self.kw['joinMethodName'] del self.kw['joinMethodName'] return self.baseClass(soClass=soClass, + joinDef=self, joinMethodName=self._joinMethodName, **self.kw) @@ -53,6 +53,7 @@ orderBy=NoDefault, joinDef=None): self.soClass = soClass + self.joinDef = joinDef self.otherClassName = otherClass classregistry.registry(soClass.sqlmeta.registry).addClassCallback( otherClass, self._setOtherClass) Modified: trunk/SQLObject/sqlobject/main.py =================================================================== --- trunk/SQLObject/sqlobject/main.py 2005-07-28 08:38:30 UTC (rev 859) +++ trunk/SQLObject/sqlobject/main.py 2005-07-31 06:58:07 UTC (rev 860) @@ -127,7 +127,7 @@ def findDependantColumns(name, klass): depends = [] - for col in klass.sqlmeta._columns: + for col in klass.sqlmeta.columnList: if col.foreignKey == name and col.cascade is not None: depends.append(col) return depends @@ -164,10 +164,26 @@ # when necessary: (bad clever? maybe) expired = False + # This is a mapping from column names to SOCol (or subclass) + # instances: + columns = {} + columnList = [] + + # This is a mapping from column names to Col (or subclass) + # instances; these objects don't have the logic that the SOCol + # objects do, and are not attached to this class closely. + columnDefinitions = {} + + # These are lists of the join and index objects: + joins = [] + indexes = [] + indexDefinitions = [] + joinDefinitions = [] + __metaclass__ = declarative.DeclarativeMeta # These attributes shouldn't be shared with superclasses: - _unshared_attributes = ['table', 'idName'] + _unshared_attributes = ['table', 'idName', 'columns'] # These are internal bookkeeping attributes; the class-level # definition is a default for the instances, instances will @@ -223,17 +239,302 @@ cls._plainJoinRemovers = {} # This is a dictionary of columnName: columnObject - cls._columnDict = {} - cls._columns = [] + # None of these objects can be shared with superclasses + cls.columns = {} + cls.columnList = [] + # These, however, can be shared: + cls.columnDefinitions = cls.columnDefinitions.copy() + cls.indexes = [] + cls.indexDefinitions = cls.indexDefinitions[:] + cls.joins = [] + cls.joinDefinitions = cls.joinDefinitions[:] + + setClass = classmethod(setClass) - # We keep track of the different joins by index, - # putting them in this list. - cls._joinList = [] - cls._joinDict = {} - cls._indexList = [] + ############################################################ + ## Adding special values, like columns and indexes + ############################################################ - setClass = classmethod(setClass) + ######################################## + ## Column handling + ######################################## + def addColumn(cls, columnDef, changeSchema=False, connection=None): + sqlmeta = cls + soClass = cls.soClass + del cls + column = columnDef.withClass(soClass) + name = column.name + assert name != 'id', ( + "The 'id' column is implicit, and should not be defined as " + "a column") + assert name not in sqlmeta.columns, ( + "The class %s.%s already has a column %r (%r), you cannot " + "add the column %r" + % (soClass.__module__, soClass.__name__, name, + sqlmeta.columnDefinitions[name], + columnDef)) + sqlmeta.columnDefinitions[name] = columnDef + sqlmeta.columns[name] = column + # A stable-ordered version of the list... + sqlmeta.columnList.append(column) + + ################################################### + # Create the getter function(s). We'll start by + # creating functions like _SO_get_columnName, + # then if there's no function named _get_columnName + # we'll alias that to _SO_get_columnName. This + # allows a sort of super call, even though there's + # no superclass that defines the database access. + if sqlmeta.cacheValues: + # We create a method here, which is just a function + # that takes "self" as the first argument. + getter = eval('lambda self: self._SO_loadValue(%s)' % repr(instanceName(name))) + + else: + # If we aren't caching values, we just call the + # function _SO_getValue, which fetches from the + # database. + getter = eval('lambda self: self._SO_getValue(%s)' % repr(name)) + setattr(soClass, rawGetterName(name), getter) + + # Here if the _get_columnName method isn't in the + # definition, we add it with the default + # _SO_get_columnName definition. + if not hasattr(soClass, getterName(name)) or (name == 'childName'): + setattr(soClass, getterName(name), getter) + sqlmeta._plainGetters[name] = 1 + + ################################################# + # Create the setter function(s) + # Much like creating the getters, we will create + # _SO_set_columnName methods, and then alias them + # to _set_columnName if the user hasn't defined + # those methods themself. + + # @@: This is lame; immutable right now makes it unsettable, + # making the table read-only + if not column.immutable: + # We start by just using the _SO_setValue method + setter = eval('lambda self, val: self._SO_setValue(%s, val, self.%s, self.%s)' % (repr(name), '_SO_fromPython_%s' % name, '_SO_toPython_%s' % name)) + setattr(soClass, '_SO_fromPython_%s' % name, column.fromPython) + setattr(soClass, '_SO_toPython_%s' % name, column.toPython) + setattr(soClass, rawSetterName(name), setter) + # Then do the aliasing + if not hasattr(soClass, setterName(name)) or (name == 'childName'): + setattr(soClass, setterName(name), setter) + # We keep track of setters that haven't been + # overridden, because we can combine these + # set columns into one SQL UPDATE query. + sqlmeta._plainSetters[name] = 1 + + ################################################## + # Here we check if the column is a foreign key, in + # which case we need to make another method that + # fetches the key and constructs the sister + # SQLObject instance. + if column.foreignKey: + + # We go through the standard _SO_get_columnName + # deal, except chopping off the "ID" ending since + # we're giving the object, not the ID of the + # object this time: + if sqlmeta.cacheValues: + # self._SO_class_className is a reference + # to the class in question. + getter = eval('lambda self: self._SO_foreignKey(self._SO_loadValue(%r), self._SO_class_%s)' % (instanceName(name), column.foreignKey)) + else: + # Same non-caching version as above. + getter = eval('lambda self: self._SO_foreignKey(self._SO_getValue(%s), self._SO_class_%s)' % (repr(name), column.foreignKey)) + if column.origName.upper().endswith('ID'): + origName = column.origName[:-2] + else: + origName = column.origName + setattr(soClass, rawGetterName(origName), getter) + + # And we set the _get_columnName version + # (sans ID ending) + if not hasattr(soClass, getterName(name)[:-2]): + setattr(soClass, getterName(name)[:-2], getter) + sqlmeta._plainForeignGetters[name[:-2]] = 1 + + if not column.immutable: + # The setter just gets the ID of the object, + # and then sets the real column. + setter = eval('lambda self, val: setattr(self, %s, self._SO_getID(val))' % (repr(name))) + setattr(soClass, rawSetterName(name)[:-2], setter) + if not hasattr(soClass, setterName(name)[:-2]): + setattr(soClass, setterName(name)[:-2], setter) + sqlmeta._plainForeignSetters[name[:-2]] = 1 + + # We'll need to put in a real reference at + # some point. See needSet at the top of the + # file for more on this. + classregistry.registry(sqlmeta.registry).addClassCallback( + column.foreignKey, + lambda foreign, me, attr: setattr(me, attr, foreign), + soClass, '_SO_class_%s' % column.foreignKey) + + if column.alternateMethodName: + func = eval('lambda cls, val, connection=None: cls._SO_fetchAlternateID(%s, %s, val, connection=connection)' % (repr(column.name), repr(column.dbName))) + setattr(soClass, column.alternateMethodName, classmethod(func)) + + if changeSchema: + conn = connection or soClass._connection + conn.addColumn(sqlmeta.table, column) + + if soClass._SO_finishedClassCreation: + makeProperties(soClass) + + addColumn = classmethod(addColumn) + + def addColumnsFromDatabase(sqlmeta, connection=None): + soClass = sqlmeta.soClass + conn = connection or soClass._connection + for columnDef in conn.columnsFromSchema(sqlmeta.table, soClass): + if columnDef.name not in sqlmeta.columnDefinitions: + sqlmeta.addColumn(columnDef) + + addColumnsFromDatabase = classmethod(addColumnsFromDatabase) + + def delColumn(sqlmeta, column, changeSchema=False, connection=None): + soClass = sqlmeta.soClass + if isinstance(column, str): + column = sqlmeta.columns[column] + if isinstance(column, col.Col): + for c in sqlmeta.columns.values(): + if column is c.columnDef: + column = c + break + else: + raise IndexError( + "Column with definition %r not found" % column) + name = column.name + del sqlmeta.columns[name] + del sqlmeta.columnDefinitions[name] + sqlmeta.columnList.remove(column) + delattr(soClass, rawGetterName(name)) + if sqlmeta._plainGetters.has_key(name): + delattr(soClass, getterName(name)) + delattr(soClass, rawSetterName(name)) + if sqlmeta._plainSetters.has_key(name): + delattr(soClass, setterName(name)) + if column.foreignKey: + delattr(soClass, rawGetterName(name)[:-2]) + if sqlmeta._plainForeignGetters.has_key(name[:-2]): + delattr(soClass, getterName(name)[:-2]) + delattr(soClass, rawSetterName(name)[:-2]) + if sqlmeta._plainForeignSetters.has_key(name[:-2]): + delattr(soClass, setterName(name)[:-2]) + if column.alternateMethodName: + delattr(soClass, column.alternateMethodName) + + if changeSchema: + conn = connection or soClass._connection + conn.delColumn(sqlmeta.table, column) + + if soClass._SO_finishedClassCreation: + unmakeProperties(soClass) + + delColumn = classmethod(delColumn) + + ######################################## + ## Join handling + ######################################## + + def addJoin(cls, joinDef): + sqlmeta = cls + soClass = cls.soClass + # The name of the method we'll create. If it's + # automatically generated, it's generated by the + # join class. + join = joinDef.withClass(soClass) + meth = join.joinMethodName + + sqlmeta.joins.append(join) + index = len(sqlmeta.joins)-1 + if joinDef not in sqlmeta.joinDefinitions: + sqlmeta.joinDefinitions.append(joinDef) + + # The function fetches the join by index, and + # then lets the join object do the rest of the + # work: + func = eval('lambda self: self.sqlmeta.joins[%i].performJoin(self)' % index) + + # And we do the standard _SO_get_... _get_... deal + setattr(soClass, rawGetterName(meth), func) + if not hasattr(soClass, getterName(meth)): + setattr(soClass, getterName(meth), func) + sqlmeta._plainJoinGetters[meth] = 1 + + # Some joins allow you to remove objects from the + # join. + if hasattr(join, 'remove'): + # A... [truncated message content] |