Я пытаюсь упростить сериализацию JSON и де-сериализацию ndb KeyProperties, используя специальный валидатор, который преобразует строку в Key
соответствующего типа.BadValueError при сохранении повторяющегося KeyProperty с пользовательским валидатором
Идея заключается в том, чтобы иметь свойство как это:
def key_validator(kind):
def validator(prop, value):
if not isinstance(value, ndb.Key):
return ndb.Key(kind, value)
return value
return validator
class Bar(ndb.Model):
foo = ndb.KeyProperty('Foo', validator=key_validator('Foo'))
Как вы можете видеть, что валидатор преобразует любую строку в Key
данного вида. Цель состоит в том, чтобы иметь возможность передать объект JSON, который содержит идентификатор ключа к populate
метода, как так:
bar = Bar()
bar.populate(json.loads('{"foo": "1234"}'))
Что должен делать это эффективно:
bar = Bar()
bar.foo = ndb.Key("Foo", "1234")
Проблема в том, что для этого требуется переопределить KeyProperty
, потому что валидатор вызывается после выполнения некоторой базовой проверки, что не работает, потому что "1234"
, по-видимому, не является Key
, см. issue 268.
Итак, чтобы сделать эту работу я создал «ValidationMixin» и новый KeyProperty, который вызывает валидатор перед тем принимает любое другое подтверждение места (а также сериализует Key
только идентификатор).
class ValidationMixin(object):
# make sure to call _validator before we do as the very first validation step
def _do_validate(self, value):
if self._validator is not None:
newvalue = self._validator(self, value)
if newvalue is not None:
value = newvalue
return super(ValidationMixin, self)._do_validate(value)
# A KeyProperty that allows a validator to generate a Key.
# In addition it serializes to just the id of the key
class KeyProperty(ValidationMixin, ndb.KeyProperty):
# return just the id of the key
def _get_for_dict(self, entity):
value = self._get_value(entity)
if self._repeated:
return [v.id() for v in value]
elif value is not None:
return value.id()
return value
Используя этот KeyProperty
работает как шарм для неповторяющихся свойств. К сожалению, он плохо справляется со свойствами, которые имеют repeated=True
.
следующее исключение, когда я звоню bar.populate(json.loads('[{"foo": "1234"}]'))
с последующим put()
:
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 3451, in _put
return self._put_async(**ctx_options).get_result()
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/tasklets.py", line 383, in get_result
self.check_success()
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/tasklets.py", line 427, in _help_tasklet_along
value = gen.throw(exc.__class__, exc, tb)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/context.py", line 824, in put
key = yield self._put_batcher.add(entity, options)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/tasklets.py", line 430, in _help_tasklet_along
value = gen.send(val)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/context.py", line 358, in _put_tasklet
keys = yield self._conn.async_put(options, datastore_entities)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/datastore/datastore_rpc.py", line 1852, in async_put
pbs = [entity_to_pb(entity) for entity in entities]
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 697, in entity_to_pb
pb = ent._to_pb()
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 3167, in _to_pb
prop._serialize(self, pb, projection=self._projection)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1422, in _serialize
values = self._get_base_value_unwrapped_as_list(entity)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1192, in _get_base_value_unwrapped_as_list
wrapped = self._get_base_value(entity)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1180, in _get_base_value
return self._apply_to_values(entity, self._opt_call_to_base_type)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1355, in _apply_to_values
newvalue = function(value)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1234, in _opt_call_to_base_type
value = _BaseValue(self._call_to_base_type(value))
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1255, in _call_to_base_type
return call(value)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1331, in call
newvalue = method(self, value)
File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 2013, in _validate
raise datastore_errors.BadValueError('Expected Key, got %r' % (value,))
BadValueError: Expected Key, got [Key('Foo', '486944fe896a44c689275e6f19e3084a')]
Как вы можете видеть, что жалуется значение является список вместо одного Key
. Обратите внимание, что Исключение выбрано в put()
не в populate
, поэтому начальная проверка, выполненная _set_value
, преуспела.
Так что мой вопрос: мой подход сломан или должен работать? Если он должен работать, почему он не работает и как он может быть исправлен?
обновление
Согласно стека проследить выполнение кода проходит model.py, line 1355, что странно, потому что свойство повторяется, и если другая ветвь в model.py, line 1347
обновления 2
Я только что обнаружил, что он работает, когда я удаляю пыльник, не повторяющийся KeyProperty
, из модели. Похоже, что сериализация сломана и неправильный экземпляр KeyProperty
передан методу _seralize