Copy an entity in Google App Engine datastore in Python without knowing property names at 'compile' time

后端 未结 7 867
庸人自扰
庸人自扰 2020-11-30 01:25

In a Python Google App Engine app I\'m writing, I have an entity stored in the datastore that I need to retrieve, make an exact copy of it (with the exception of the key), a

相关标签:
7条回答
  • 2020-11-30 01:40

    Here's the code provided by @zengabor with the if expression formatted for easier reading. It may not be PEP-8 compliant:

    klass = e.__class__
    props = {}
    for k, v in klass.properties().iteritems():
        if not (type(v) == db.DateTimeProperty and ((
                skip_auto_now     and getattr(v, 'auto_now'    )) or (
                skip_auto_now_add and getattr(v, 'auto_now_add')))):
            if type(v) == db.ReferenceProperty:
                value = getattr(klass, k).get_value_for_datastore(e)
            else:
                value = v.__get__(e, klass)
            props[k] = value
    props.update(extra_args)
    return klass(**props)
    
    0 讨论(0)
  • 2020-11-30 01:44

    I'm neither Python nor AppEngine guru, but couldn't one dynamically get/set the properties?

    props = {}
    for p in Thing.properties():
        props[p] = getattr(old_thing, p)
    new_thing = Thing(**props).put()
    
    0 讨论(0)
  • 2020-11-30 01:49

    Here you go:

    def clone_entity(e, **extra_args):
      """Clones an entity, adding or overriding constructor attributes.
    
      The cloned entity will have exactly the same property values as the original
      entity, except where overridden. By default it will have no parent entity or
      key name, unless supplied.
    
      Args:
        e: The entity to clone
        extra_args: Keyword arguments to override from the cloned entity and pass
          to the constructor.
      Returns:
        A cloned, possibly modified, copy of entity e.
      """
      klass = e.__class__
      props = dict((k, v.__get__(e, klass)) for k, v in klass.properties().iteritems())
      props.update(extra_args)
      return klass(**props)
    

    Example usage:

    b = clone_entity(a)
    c = clone_entity(a, key_name='foo')
    d = clone_entity(a, parent=a.key().parent())
    

    EDIT: Changes if using NDB

    Combining Gus' comment below with a fix for properties that specify a different datastore name, the following code works for NDB:

    def clone_entity(e, **extra_args):
      klass = e.__class__
      props = dict((v._code_name, v.__get__(e, klass)) for v in klass._properties.itervalues() if type(v) is not ndb.ComputedProperty)
      props.update(extra_args)
      return klass(**props)
    

    Example usage (note key_name becomes id in NDB):

    b = clone_entity(a, id='new_id_here')
    

    Side note: see the use of _code_name to get the Python-friendly property name. Without this, a property like name = ndb.StringProperty('n') would cause the model constructor to raise an AttributeError: type object 'foo' has no attribute 'n'.

    0 讨论(0)
  • 2020-11-30 01:49

    If you're using the NDB you can simply copy with: new_entity.populate(**old_entity.to_dict())

    0 讨论(0)
  • 2020-11-30 01:51

    This can be tricky if you've renamed the underlying keys for your properties... which some people opt to do instead of making mass data changes

    say you started with this:

    class Person(ndb.Model):
       fname = ndb.StringProperty()
       lname = ndb.StringProperty()
    

    then one day you really decided that it would be nicer to use first_name and last_name instead... so you do this:

    class Person(ndb.Model):
       first_name = ndb.StringProperty(name="fname")
       last_name = ndb.StringProperty(name="lname")
    

    now when you do Person._properties (or .properties() or person_instance._properties) you will get a dictionary with keys that match the underlying names (fname and lname)... but won't match the actual property names on the class... so it won't work if you put them into the constructor of a new instance, or use the .populate() method (the above examples will break)

    In NDB anyways, instances of models have ._values dictionary which is keyed by the underlying property names... and you can update it directly. I ended up with something like this:

        def clone(entity, **extra_args):
            klass = entity.__class__
            clone = klass(**extra_args)
            original_values = dict((k,v) for k,v in entity._values.iteritems() if k not in clone._values)
            clone._values.update(original_values)
            return clone
    

    This isn't really the safest way... as there are other private helper methods that do more work (like validation and conversion of computed properties by using _store_value() and _retrieve_value())... but if you're models are simple enough, and you like living on the edge :)

    0 讨论(0)
  • 2020-11-30 01:53

    This is just an extension to Nick Johnson's excellent code to address the problems highlighted by Amir in the comments:

    1. The db.Key value of the ReferenceProperty is no longer retrieved via an unnecessary roundtrip to the datastore.
    2. You can now specify whether you want to skip DateTime properties with the auto_now and/or auto_now_add flag.

    Here's the updated code:

    def clone_entity(e, skip_auto_now=False, skip_auto_now_add=False, **extra_args):
      """Clones an entity, adding or overriding constructor attributes.
    
      The cloned entity will have exactly the same property values as the original
      entity, except where overridden. By default it will have no parent entity or
      key name, unless supplied.
    
      Args:
        e: The entity to clone
        skip_auto_now: If True then all DateTimeProperty propertes will be skipped which have the 'auto_now' flag set to True
        skip_auto_now_add: If True then all DateTimeProperty propertes will be skipped which have the 'auto_now_add' flag set to True
        extra_args: Keyword arguments to override from the cloned entity and pass
          to the constructor.
      Returns:
        A cloned, possibly modified, copy of entity e.
      """
    
      klass = e.__class__
      props = {}
      for k, v in klass.properties().iteritems():
        if not (type(v) == db.DateTimeProperty and ((skip_auto_now and getattr(v, 'auto_now')) or (skip_auto_now_add and getattr(v, 'auto_now_add')))):
          if type(v) == db.ReferenceProperty:
            value = getattr(klass, k).get_value_for_datastore(e)
          else:
            value = v.__get__(e, klass)
          props[k] = value
      props.update(extra_args)
      return klass(**props)
    

    The first if expression is not very elegant so I appreciate if you can share a better way to write it.

    0 讨论(0)
提交回复
热议问题