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
   :doc:`special fields </usage/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
   :py:class:`Field <pyfactory.field.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.
