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] |