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.