# Smart Relationships

{% hint style="success" %}
This is the official documentation of the `forestadmin-agent-django` and `forestadmin-agent-flask` Python agents.
{% endhint %}

Smart relationships are very different between the two versions of the Agent.

{% hint style="info" %}
You can find the full documentation of relationship customization [here](https://docs.forestadmin.com/developer-guide-agents-python/agent-customization/relationships).
{% endhint %}

## Structure

Smart relationships on legacy agents were declared creating a smart field with a `reference` property but differed in the way that:

* Relationships to a single record (many-to-one or one-to-one) worked using the `get` function which needed to return a single record.
* Relationships to a list of records (one-to-many or many-to-many) worked by implementing all the CRUD routes on a router file.

The new system is completely different: it is based on primary keys and foreign keys.

## Migrating

### Relationships when the foreign key is accessible

{% tabs %}
{% tab title="Before" %}

```python
from app.models import Address
from django_forest.utils.collection import Collection

# Many to one relationships
class OrderForest(Collection):
    def load(self):
        self.fields = [
            {
                "field": "delivery_address",
                "type": "String",
                "reference": "Address.id",
                "get": self.get_delivery_address,
            }
        ]

    def get_delivery_address(self, obj):
        return obj.delivery_address

# Reverse relationship
class AddressForest(Collection):
    def load(self):
        self.fields = [
            {
                "field": "orders",
                "type": ["String"],
                "reference": "Order.id",
            }
        ]

# urls.py
from app.views import AddressOrders

urlpatterns = [
    path(
        '/forest/address/<pk>/relationships/orders',
        AddressOrdersView.as_view(),
        name='address_orders'
    ),
    # ...
]

# views.py
from django.http import JsonResponse
from django.views import generic
from django_forest.resources.utils.queryset import PaginationMixin
from django_forest.utils.schema.json_api_schema import JsonApiSchema

from app.models import Order

class AddressOrdersView(PaginationMixin, generic.ListView):
    def get(self, request, pk, *args, **kwargs):
        params = request.GET.dict()
        queryset = Orders.objects.filter(delivery_address_id=pk)

        # pagination
        queryset = self.get_pagination(params, queryset)

        # json api serializer
        Schema = JsonApiSchema.get('orders')
        data = Schema().dump(queryset, many=True)

        return JsonResponse(data, safe=False)
```

{% endtab %}

{% tab title="After" %}

```python
# Create the relationship
agent.customize_collection("order").add_many_to_one_relation(
    "delivery_address", "Address", "delivery_address_id"
)

# Create the reverse relationship
agent.customize_collection("Address").add_one_to_many_relation(
    "orders", "Order", "delivery_address_id"
)
```

{% endtab %}
{% endtabs %}

### Relationships when you need complex logic to get a foreign key

In this example, we want to create a relationship between the `order` collection and the `address` collection (assuming that it does not already exist in the database because depends on complex logic).

We can see that in the legacy agent, the `delivery_address` field was a smart field that returned the full address of the order, while in the new agent, we will create a computed field that will contain the address ID (the foreign key), and then create the relationship.

We won't be detailing the migration of a relation to a list of records here, but it is very similar to the one described below.

{% hint style="info" %}
If the foreign key was already present in the database in a related table, use the [import-rename-delete](https://docs.forestadmin.com/developer-guide-agents-python/agent-customization/fields/import-rename-remove) feature to move it to the correct collection instead of using a computed field.

This will be much faster and will not require `In` filter operators to be implemented (as unlike computed fields, imported fields are natively filterable and sortable).
{% endhint %}

{% tabs %}
{% tab title="Before" %}

```python
from app.models import Address
from django_forest.utils.collection import Collection

# Many to one relationships
class OrderForest(Collection):
    def load(self):
        self.fields = [
            {
                "field": "delivery_address",
                "type": "String",
                "reference": "Address.id",
                "get": self.get_delivery_address,
            }
        ]

    def get_delivery_address(self, obj):
        return Address.objects.filter( """complex_query""" )
```

{% endtab %}

{% tab title="After" %}

```python
from typing import List, Dict

from app.models import Address
from forestadmin.datasource_toolkit.context.collection_context import CollectionCustomizationContext
from forestadmin.datasource_toolkit.interfaces.records import RecordsDataAlias

def get_delivery_address_id(
    records: List[RecordsDataAlias], context: CollectionCustomizationContext
):
    addresses_by_order_id = Address.objects.filter("""complex_query_here""")
    return [addresses_by_order_id[order["id"]]["id"] for order in records]

# Create a computed field that will contain the address ID (the foreign key)
agent.customize_collection("order").add_field("delivery_address_id", {
    "column_type": "Number",
    "dependencies": ["id"],
    "get_values": get_delivery_address_id
}).replace_field_operator(
    # Make the field filterable (this is required for the relationship to work)
    "delivery_address_id", "in",
    lambda value, context: pass  # implement the reverse-lookup logic here
).add_many_to_one_relation(
    # Create the relationship
    "delivery_address", "Address", "delivery_address_id"
)
```

{% endtab %}
{% endtabs %}
