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
UserFactoryThe schema to build is
basicThe 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.