Source code for invenio_records.systemfields.relatedmodelfield
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020-2021 CERN.
# Copyright (C) 2020 Northwestern University.
#
# Invenio-Records is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
"""Related model field.
The related model field serializes/dumps a related object into the record JSON
dictionary (basically denormalization). This way, the object can be recreated
directly from the record JSON dictionary instead of querying the database.
This is useful for instance during search queries to avoid hitting the
database. In addition you can subclass this field to provide life-cycle hooks
on the record. For instance a PIDField, could create the related persistent
identifier.
"""
from invenio_db import db
from sqlalchemy import inspect
from .base import SystemField, SystemFieldContext
[docs]class RelatedModelFieldContext(SystemFieldContext):
"""Context for RelatedModelField.
This class implements the class-level methods available on a
RelatedModelField. I.e. when you access the field through the class, for
instance:
.. code-block:: python
Record.myattr.session_merge(record)
"""
[docs] def session_merge(self, record):
"""Merge the PID to the session if not persistent."""
obj = self.field.obj(record)
if not inspect(obj).persistent:
obj = db.session.merge(obj)
self.field._set_cache(record, obj)
[docs]class RelatedModelField(SystemField):
"""Related model system field."""
[docs] def __init__(
self, model, key=None, required=False, load=None, dump=None, context_cls=None
):
"""Initialize the field.
:param model: Related SQLAlchemy model.
:param key: Name of key in the record to serialize the related object
under.
:param required: Flag to determine if a related object is required on
record commit time.
:param load: Callable to load the related object from a JSON object.
:param dump: Callable to dump the related object as a JSON object.
:param context_cls: The context class is used to provide additional
methods on the field itself.
"""
self._model = model
self._required = required
self._load = load or model.load_obj
self._dump = dump or model.dump_obj
self._context_cls = context_cls or RelatedModelFieldContext
super().__init__(key=key)
#
# Life-cycle hooks
#
[docs] def pre_commit(self, record):
"""Called before a record is committed."""
# Make sure we serialize/dump the related objet on record.commit() time
# as it might have changed.
related_obj = getattr(record, self.attr_name)
if related_obj is not None:
self.set_obj(record, related_obj)
elif self._required:
raise RuntimeError("You must provide a related object.")
#
# Helpers
#
[docs] def obj(self, record):
"""Get the related object.
Uses a cached object if it exists.
IMPORTANT: By default, if the object is loaded from the record JSON
object instead of from the database model, it is NOT added to the
database session. Thus, the related object will be in a transient state
instead of persistent state. This is useful for instance in search
queries to avoid hitting the database, however if you need to make
operations on it you should add it to the session using:
.. code-block::
Record.myattr.session_merge(record)
"""
# Check cache
obj = self._get_cache(record)
if obj:
return obj
obj = self._load(self, record)
if obj:
# Cache object
self._set_cache(record, obj)
return obj
return None
[docs] def set_obj(self, record, obj):
"""Set the object."""
assert isinstance(obj, self._model)
# Store data values on the attribute name (e.g. 'type') using dump
# method provided by either to the field or a function on the related
# object.
self._dump(self, record, obj)
# Cache object
self._set_cache(record, obj)
#
# Data descriptor methods (i.e. attribute access)
#
[docs] def __get__(self, record, owner=None):
"""Get the persistent identifier."""
if record is None:
return self._context_cls(self, owner)
return self.obj(record)
[docs] def __set__(self, record, pid):
"""Set persistent identifier on record."""
self.set_obj(record, pid)