Default model services¶
When the entire body of your create / update / delete service is a one-line wrapper over the mutation helpers, the framework ships ready-made factories so you don't have to write the boilerplate.
When to use them¶
Use a default factory when the service body is the canonical glue:
def create_author(*, data, **_):
return create_from_input(Author, data).instance
def update_author(*, instance, data, **_):
return update_from_input(instance, data).instance
def delete_author(*, instance, **_):
instance.delete()
Keep writing custom services the moment you need anything else:
side-effects, denormalised fields, cross-table updates, calls to other
services, conditional request.user-based behaviour, anything that benefits
from a real function name in a stack trace.
Before / after¶
Before — hand-written stubs:
from rest_framework_services import (
SelectorKind,
SelectorSpec,
ServiceSpec,
ServiceViewSet,
create_from_input,
update_from_input,
)
from myapp.models import Author
def create_author(*, data: AuthorIn, **_):
return create_from_input(Author, data).instance
def update_author(*, instance: Author, data: AuthorIn, **_):
return update_from_input(instance, data).instance
def delete_author(*, instance: Author, **_):
instance.delete()
_author_out = SelectorSpec(
kind=SelectorKind.RETRIEVE,
output_serializer=AuthorOutSerializer,
)
class AuthorViewSet(ServiceViewSet):
queryset = Author.objects.all()
action_specs = {
"create": ServiceSpec(
service=create_author,
input_serializer=AuthorInSerializer,
output_selector_spec=_author_out,
),
"update": ServiceSpec(
service=update_author,
input_serializer=AuthorInSerializer,
output_selector_spec=_author_out,
),
"destroy": ServiceSpec(service=delete_author),
}
After — factories:
from rest_framework_services import (
SelectorKind,
SelectorSpec,
ServiceSpec,
ServiceViewSet,
create_model,
delete_model,
update_model,
)
from myapp.models import Author
_author_out = SelectorSpec(
kind=SelectorKind.RETRIEVE,
output_serializer=AuthorOutSerializer,
)
class AuthorViewSet(ServiceViewSet):
queryset = Author.objects.all()
action_specs = {
"create": ServiceSpec(
service=create_model(Author),
input_serializer=AuthorInSerializer,
output_selector_spec=_author_out,
),
"update": ServiceSpec(
service=update_model(Author),
input_serializer=AuthorInSerializer,
output_selector_spec=_author_out,
),
"destroy": ServiceSpec(service=delete_model(Author)),
}
The returned callables conform to the unified
CreateService / UpdateService / DeleteService
Protocols — they accept any framework-pool keys (request, user, URL
kwargs, ServiceSpec.kwargs returns) and ignore the ones they don't need.
Sync vs async¶
Each factory ships in two flavours: the sync form (create_model,
update_model, delete_model) returns a regular callable; the async form
(acreate_model, aupdate_model, adelete_model) returns an async def
callable. The framework's is_async detection routes the
latter through the async dispatch path automatically.
Configuration¶
field_map and exclude_fields¶
Forwarded straight to the underlying mutation helper:
m2m¶
Accepts either a static mapping (passed through) or a callable that receives
the validated data and returns the mapping. The callable form is the
common case where the M2M values live on the input itself:
@dataclass
class CreatePostIn:
title: str
body: str
tags: list[Tag]
create_model(
Post,
exclude_fields=["tags"],
m2m=lambda data: {"tags": data.tags},
)
update_fields (update only)¶
Same semantics as update_from_input — True for changed-fields save,
False for full save, an explicit list to control exactly which columns
write:
soft_delete (delete only)¶
Optional hook that replaces instance.delete() — the common archive case:
def _archive(post: Post) -> None:
post.is_archived = True
post.save(update_fields=["is_archived"])
delete_model(Post, soft_delete=_archive)
The async variant takes an async def hook.
When not to use¶
If you need request.user to stamp created_by on the instance, or the
service has to coordinate with another service, write the function out by
hand. The factories cover the boilerplate case; they're not a framework
within the framework.