Associations

Any models for non-trivial application are going to require associations of some sort. It doesn’t matter if you’re using a relational database, a key-value store, or a document store. There is always data that is associated in some way.

This is really the killer feature of PyFactory: The ability to create complex graphs of associations for a model on the fly. A real-world example which kicked off the building of this library: An ApplicationAchievement requires an Achievement and a Session, which in turn requires an Application which furthermore requires a User. So if we were writing unit tests for an ApplicationAchievement we could either create all these by hand, or mock them out. With PyFactory, you just write the factory for all the models, link them together using associations, and the rest is done for you!

Basic Association

Let’s look at a basic association: a Post has a column which is a foreign key called author_id which points to a User. Assuming the UserFactory is already written, here is the PostFactory:

from pyfactory import Factory, schema, association

class PostFactory(Factory):
    @schema()
    def basic(self):
        return {
            "title": "My Post",
            "author_id": association(UserFactory(), "basic", "id")
        }

The key line is the association line. This tells PyFactory three important things:

  • The association can be built from the UserFactory
  • The schema to build is basic
  • The attribute to use is id

The result of this is that the author_id field will end up being replaced with the “id” attribute for the “basic” schema from the UserFactory. Nifty!

The attribute parameter can also be ommitted, which will then simply use the entire model as the value of the field. This is particularly useful for document stores where you build up each part of the document piecemeal.

Note

Since schemas return the general structure of your application, rather than direct value, this association doesn’t return the model instance right away. Instead, the association is not built until the schema is realized.

For more information on how this works, read the documentation on special fields.

Multiple Fields from a Single Association

Sometimes a model may use multiple fields from a single model. This is, for example, common with document stores where instead of using joins for relational data, it is often common to sacrafice some small data normalization for a document structure. As an example, a comment to a blog post may store the name and email of the author with the comment instead of a foreign key to a user document.

In this case, using two association calls wouldn’t be appropriate since this would cause PyFactory to create two different records when you really want the value of two different attributes from a single record. An example of this exact case is shown below:

class CommentFactory(Factory):
    @schema()
    def comment(self):
        user = association(UserFactory(), "basic")

        return {
            "body": "This is my comment.",
            "name": user.attribute("name"),
            "email": user.attribute("email")
        }

This allows you to easily get multiple attributes of a single association.

Note

You can note assign the return value of an attribute call to a variable. The result is not the actual value of the attribute as you would expect. Since schema methods simply return the structure of a model, it has to encapsulate associations as such in your schema. To that end, a call to attribute actually returns a reference to a Field instance.

Building a Field from an Association’s Data

Another common case is that an attribute of an associated model may not actually be used directly, but instead be used to build the value itself. For example, going back to our comment example above: What if the User object had a first_name and last_name field, but we wanted to store the full name in the comment, in a single field? With what we’ve seen so far, we would be out of luck.

Associations have another trick up their sleeve: callbacks. You can provide a callback to an association, which will be called with an attributes dictionary you can actually use to build up values.

class CommentFactory(Factory):
    @schema()
    def comment(self):
        def get_email(user):
            return "%s %s" % \
                (user["first_name"], user["last_name"])

        user = association(UserFactory(), "basic")

        return {
            "body": "This is my comment.",
            "name": user.callback(get_email)
        }

In this case, its hopefully clear to see that the get_email callback will be called, passing in a dictionary of attributes for the user. The return value of the callback will be used for the actual value of the field.