Open jasonmyers opened 9 years ago
Good idea, I think we should add such functionality. I'll think about it.
+1
+1
Hi. Has this been considered? Currently it's the one thing that's stopping me from using Pony in my project.
Thanks.
Any update about it?
EDIT: @VincentSch4rf provided a much cleaner solution below, assuming you don't mind the minor limitation I describe in my next comment.
For anyone still waiting for this, you can half accomplish it with monkey-patching. Not a perfect solution, but it gets me what I want (UUID IDs and timestamps on all classes without having to duplicate code):
# my_project/models.py
from pony import orm
from my_project.mixins import uuid_with_timestamps
db = orm.Database()
# All entity classes defined after this point will have a UUID ID and timestamps
uuid_with_timestamps()
class User(db.Entity):
username = orm.Required(str)
password_hash = orm.Required(str)
# my_project/mixins.py
import uuid
from datetime import datetime
from pony import orm
old_init = orm.core.EntityMeta.__init__
def uuid_with_timestamps():
def shared_before_insert(self):
self.updated_at = self.created_at
def shared_before_update(self):
self.updated_at = datetime.now()
def new_init(entity_cls, cls_name, cls_bases, cls_dict):
entity_cls.uuid = orm.PrimaryKey(uuid.UUID, default=uuid.uuid4)
entity_cls.created_at = orm.Required(datetime, default=datetime.now)
entity_cls.updated_at = orm.Optional(datetime)
entity_cls.before_insert = shared_before_insert
entity_cls.before_update = shared_before_update
old_init(entity_cls, cls_name, cls_bases, cls_dict)
orm.core.EntityMeta.__init__ = new_init
Caveats: I haven't tested this with inheritance (I have no intention of using that) and I don't know yet how hard the unit tests will be. Also, the shared before_
methods will overwrite any customizations you try to make. There are ways around that, but I wanted to keep this simple.
You can also achieve this by creating a custom EntityMeta
class, injecting the common fields into the attrs
before passing it to the class' constructor like this:
class CustomMeta(EntityMeta):
def __new__(metacls, name: str, bases, attrs):
attrs['pk1'] = Required(datetime)
attrs['pk2'] = Required(int)
return super().__new__(metacls, name, bases, attrs)
By overriding the __init__
as well, you can also set composite primary keys, which are shared by a set of entities. I found this particularly useful when working with timescaledb.
class CustomMeta(EntityMeta):
...
def __init__(cls, name, bases, cls_dict, **kwargs):
indexes = [Index("pk1", "pk2", is_pk=True)]
setattr(cls, "_indexes_", indexes)
super(CustomMeta, cls).__init__(name, bases, cls_dict)
I would still love this to be supported natively :smile:
@VincentSch4rf Interestingly, I had already tried that and discarded it, but forgot why. It turns out that some code deep in Pony rejects some queries on any models which do not explicitly have EntityMeta
as their type (i.e. subclassed metas don't count). The only failing query I've found so far is User.select()
, so it may be worth it for others. If so, here's a full minimalist example showing both it working (both shared attributes and shared before_insert) and the failure I mentioned:
import uuid
from datetime import datetime, timezone
from pony import orm
db = orm.Database()
class CustomMeta(orm.core.EntityMeta):
def __new__(metacls, name, bases, attrs):
attrs["uuid"] = orm.PrimaryKey(uuid.UUID, default=uuid.uuid4)
old_before_insert = attrs.get("before_insert")
def new_before_insert(self):
self.updated_at = datetime.now(timezone.utc)
if old_before_insert:
old_before_insert()
attrs["updated_at"] = orm.Optional(datetime)
attrs["before_insert"] = new_before_insert
return super().__new__(metacls, name, bases, attrs)
class User(db.Entity, metaclass=CustomMeta):
username = orm.Required(str)
if __name__ == "__main__":
db.bind(provider="sqlite", filename=":memory:")
db.generate_mapping(create_tables=True)
with orm.db_session:
User(username="blah")
print(list(orm.select((u.uuid, u.username, u.updated_at) for u in User)))
# Fails in pony.orm.core.extract_vars due to pony.orm.ormtypes.normalize
# not checking for subclass metaclasses
print(list(User.select()))
And yes, having it supported natively would be amazing.
This might be an enhancement, but is there any way for an abstract entity (i.e. a mixin) that can contribute methods/columns to an entity, but doesn't show in the inheritance hierarchy? e.g. as in Django https://docs.djangoproject.com/en/1.7/topics/db/models/#abstract-base-classes
An example use case would be a created/updated timestamp mixin, e.g. something like
where
Timestamped
can be re-used across multiple Entities as a mixin, can't be instantiated or queried, and doesn't trigger the_discriminator_
code