mixxorz/django-service-objects

Add ModelField that accepts model instances instead of PKs

Closed this issue · 2 comments

Currently, if you wanted to accept model instances as inputs to your services, you'd have to use ModelChoiceField. It works well enough, but it has its quirks. It would be a good idea to a have a custom field that can accept model instances directly. This would fix a few issues I've found when using ModelChoiceField.

Prevent unintended model mixups

Because ModelChoiceField just validates using the primary key, it's really easy to mix up models.

person = Person.objects.get(pk=1)
tenant = Tenant.objects.get(pk=1)

SendInvoice.execute({'person': tenant.pk})
# meant to pass in person.pk, but because it's just an integer, it still validates

If we pass in model instances, we can check for its type.

person = Person.objects.get(pk=1)
tenant = Tenant.objects.get(pk=1)

SendInvoice.execute({'person': tenant})
# raises InvalidInputsError, person is not an instance of Person

Allow unsaved model instances as inputs

Because ModelChoiceField fetches your model via its PK, it inherently requires that your model already be saved before being passed in.

With a custom field, you could accept unsaved model instances as inputs, as long as they're valid.

person = Person(
    name='Mitch',
    age=24,
)

SendInvoice.execute({'person': person})
# works

person = Person(
    name=1234,
)

SendInvoice.execute({'person': person})
# raises InvalidInputsError (name should be string, age is required)

Prevent multiple calls to the database

Often I find that I first need to fetch a model instance before I can pass them as inputs. This means that two database queries are made to fetch the same data.

person = Person.objects.get(name='Mitch')  # fetch object from the database

SendInvoice.execute({'person': person.pk})  # the form would have to fetch the data from the database again

Solution: ModelField

I feel like it would be relatively straightforward to implement a custom field that accepts a model instance as the parameter.

How I imagine the API to be:

class SendInvoice(Service):
    person = ModelField('people.Person', allow_unsaved=True)

# to use
person = Person.objects.get(name='Mitch')

SendInvoice.execute({'person': person})

If someone could help with this, that'd be great.

c17r commented

So basically a field that does type checking, the allow_unsaved verification, and then proxies everything else to the underlying object?

c17r commented

Nevermind on the proxying part, thinking of a different project. I took a stab at this, about to submit the PR