redis / redis-om-python

Object mapping, and more, for Redis and Python
MIT License
1.12k stars 112 forks source link

clean way to add non-persistable fields? #325

Open melder opened 2 years ago

melder commented 2 years ago

I want to initialize my model with extra attributes that are inferred from persisted fields

For example, let's say I have birthdate and I want to calculate age. There is no reason to store both fields, so I can do the following:

import datetime
from typing import Optional

from pydantic import EmailStr

from redis_om import HashModel

class Customer(HashModel):
    first_name: str
    last_name: str
    email: EmailStr
    join_date: datetime.date
    birth_date: datetime.date
    bio: Optional[str]

def __init__(self, **kwargs):
    super().__init__(**kwargs)
    self.age = helper_func.get_age_from_birthdate(self.birthdate)

The problem is with the above approach is "age" gets persisted.

I can do the following:

class Customer(HashModel):
    first_name: str
    last_name: str
    email: EmailStr
    join_date: datetime.date
    birth_date: datetime.date
    bio: Optional[str]

def populate_extra_fields(self):
    self.age = helper_func.get_age_from_birthdate(self.birthdate)

c = Customer.get(id)
c.populate_extra_fields()

But then on subsequent .save() the age field gets persisted. I will have to re-fetch to update the model without age getting persisted, e.g:

# BAD: (age gets persisted)
c = Customer.get(id)
c.populate_extra_fields()
if c.age > 35:
    c.first_name = "melder"
    c.save()

# GOOD: (age does not get persisted)
c = Customer.get(id)
c.populate_extra_fields()
if c.age > 35:
    c = Customer.get(id)
    c.first_name = "melder"
    c.save()

Is this simply outside the scope of the library or how it should be used? Would it better to write a wrapper to handle these extra fields?

melder commented 2 years ago

Ok so I tried the wrapper approach and it seems to work. Here an example:

class CustomerWrapper:
    """
    Wrapper to extend redis_om functionality since its behavior is somewhat magical.
    Best strategy is to keep the models as concise as possible.
    """
    class Customer(HashModel):
        """
        Customer model
        """
        first_name:  str
        last_name:   str
        email:       EmailStr
        join_date:   datetime.date
        birth_date:  datetime.date
        bio:         Optional[str]

    customer_fields = list(Customer.__fields__.keys())

    def __init__(self, _customer):
        self.customer = _customer
        self.age = None

        for k, v in vars(self.customer).items():
            if k in self.customer_fields:
                setattr(self, k, v)

        self.age = helper.birthdate_to_age(self.birth_date)

    @classmethod
    def parse(cls, **kwargs):
        return { k: kwargs[k] for k in cls.customer_fields if k in kwargs }

    @classmethod
    def new(cls, **kwargs):
        return cls(cls.Customer(**cls.parse(kwargs)))

    def save(self, **kwargs):
        for k, v in (kwargs or vars(self)).items():
            if k in self.customer_fields:
                setattr(self, k, v)
                setattr(self.customer, k, v)
        self.customer.save()
        return self

def new(**kwargs):
    return CustomerWrapper.new(**kwargs)

customer_data = { 'firstname': "melder", ... }

customer = new(**customer_data).save()
print(customer.age)

So CustomerWrapper should behave similarly to Customer, and Customer methods can be extended in CustomerWrapper or by accessing them through the "customer instance field", e.g:

c = customer.customer.get(id)
sav-norem commented 2 years ago

@simonprickett seems like this would be a great example for us to highlight in some documentation but isn't currently an issue

melder commented 2 years ago

@sav-norem Yea feel free to close this. But I'd update the docs with a warning that all instance vars of the OM get persisted.

I'm still relatively new to python so if I have the time I'll look into implementing this as a decorator and package it as a separate plugin module- unless you have a better approach in mind and can offer guidance. Let me know how I can help.