liberation/redis-limpyd — pull request #41 by twidi (2012-08-12 11:57:09+00:00)
Title: Related fields

I think it's the bigger PR for limpyd: adding "related fields"

To start, a little example.

Given these two models:

from limpyd import fields
from limpyd.contrib import related

class Person(related.RelatedModel):
database = main_database
name = fields.PKField() # redefine a PK just for the example

class Group(related.RelatedModel):
database = main_database
name = fields.PKField()
private = fields.StringField()
owner = related.FKHashableField('Person')
members = related.M2MSetField('Person', related_name='membership')

You can them it this way:

>>> core_devs = Group(name='limpyd core devs', private=0)
>>> ybon = Person(name='ybon')
>>> twidi = Person(name='twidi')
>>> core_devs.owner.hset(ybon)
1
>>> core_devs.members.sadd(twidi, ybon._pk) # give a limpyd object, or a pk
2
>>> core_devs.members.smembers()
set(['ybon', 'twidi'])
>>> ybon.group_set() # it's a collection, the limpyd way !
['limpyd core devs']
>>> twidi.membership() # it's a collection too
['limpyd core devs']

The related stuff provide one base model, `RelatedModel`, which handle creation of related collections (simple shortcuts to real collections, see later), and deletion/cleanup of reverse side.

In addition to this `RelatedModel`, we have 5 news fields, grouped in two parts FK and M2M, for each base field:

- `FKStringField`
- `FKHashableField`
- `M2MSetField`
- `M2MListField`
- `M2MSortedField`

All these fields are stored, and used, the exact same way as their base field (ie e `FKStringField` acts as (in fact "is") a `StringField`, a `M2MSortedField` as a `SortedField`)

The only real difference is that you can pass limpyd objects as values, instead of only string. Of course you can still pass whatever you want, but for the related stuff to work, you should pass PK of objects, or objects themselves (translated to their pk). In the example above, we had this ligne:

>>> core_devs.members.sadd(twidi, ybon._pk)

Here, `twidi` is an object, which will be translated to `twidi._pk`, and ` ybon._pk` will be kept as is.

There is no control of the value when setting a related field. It's up to you to take care of this. Limpyd is not a SGBDR !

All these related fields are indexable, it's the heart of all of this. When setting `ybon._pk` to the `core_devs.owner` field, the collection linked to this field is updated (it's a limpyd's core stuff, nothing new here). So we can do this:

>>> Group.collection(members=ybon._pk)

to retrieve all groups owned by ybon. So, why new fields, if it works the same ? Because with related fields, we create a `RelatedCollection` on the reverse side of the relation. Wo we can write the exact same operation this way:

>>> ybon.membership()

It's simply a shortcut to the `Group.collection` call, and so can accept other arguments to precise your filters:

>>> Group.collection(members=ybon._pk, private=0)
>>> # or
>>> ybon.membership(private=0)

The related collection, with the related model, has an other goal: manage the deletion. In our example, if a `Person` is deleted, the `Group.members` field will be updated:

>>> twidi.delete()
>>> core_devs.members.smembers()
set(['ybon'])

Without related model/fields/collection, we should have do:

>>> core_devs.members.srem(twidi._pk)
>>> twidi.delete()

It's just one-line benefit, but it helps to keep consistency in our data.

Now a word or two about the attributes of `RelatedField` based fields.

- The first argument, `to`, is mandatory and indicate on which model we want our will to be related. can be either a real model (ie `Group`), or a string with (ie `"mynamespace:Group"`) or without (ie `"Group"`) namespace. Without namespace, we use the namespace of the model of the related field. You can also pass `"self"` to indicate a relation over the same model as the related field.
- The second argument, `related_name` is optional, but sometimes mandatory. It's the name of the related collection on the related model, as `membership` for the `Group.members` field, which is available on all `Person` objects. If set to a full plain string, it must be a valid python identifier and will be used as is. If it contains `%(namespace)s` and/or `%(model)s`, these two will be replaced at runtime by the namespace and model of the model owning the related fields. It's usefull when the related field is declared in a abstract class. And it no related name is given, we simply use the string `%s(model)s_set`. I said this argument is sometimes mandatory, because two fields cannot have the same one, so if two related fields point to the same domain, the default related_name cannot be used for the two, you must define at least one of the two.

To finish, note that related fields can only point to models on the same database.

PS: this PR comes with 28 tests ;)