pynamodb / PynamoDB

A pythonic interface to Amazon's DynamoDB
http://pynamodb.readthedocs.io
MIT License
2.45k stars 427 forks source link

How to update fields dynamically just like serializers do in Django? #1154

Open satyam8080 opened 1 year ago

satyam8080 commented 1 year ago

I have a University model which has some fields like campus, faculty, etc and I want to update it without manually checking or by looping over each request payload.

For Example -

`class CampusMap(DynamicMapAttribute):

campus_id = UnicodeAttribute(null=True)
address = UnicodeAttribute(null=True)
city = UnicodeAttribute(null=True)
state = UnicodeAttribute(null=True)
country = UnicodeAttribute(null=True)
email = UnicodeAttribute(null=True)
phone = UnicodeAttribute(null=True)`

`class UniversityModel(Model):

class Meta:        

    table_name = 'University'
    region = os.getenv('DB_REGION_NAME')
    aws_access_key_id = os.getenv('DB_ACCESS_KEY_ID')
    aws_secret_access_key = os.getenv('DB_SECRET_ACCESS_KEY')

uid = UnicodeAttribute(hash_key=True)
university = UniversityMap(null=True)
courses = CourseMap(null=True)
branch = BranchMap(null=True)
university_jobs = UniversityJobMap(null=True)
visited_companies = VisitedCompanyMap(null=True)
university_students = StudentMap(null=True)
faculty = FacultyMap(null=True)
campus = CampusMap(null=True)
faq = FaqMap(null=True)
subscriptions = SubscriptionMap(null=True)
created_at = UTCDateTimeAttribute(default=datetime.utcnow())
updated_at = UTCDateTimeAttribute(default=datetime.utcnow())

`

Now consider the request payload for updating the campus will be like this - { "city": "Muzaffarpur" } Or, It can be - { "state": "Bihar" }

So Is it any way to dynamically update the campus without checking the keys in the payload, like if the city is present then only the city will be updated, or if both city and state are present then both will be updated?

I want something like Serializers does in the case of Django + Postgres, we need to pass the payload data along with the University object.

Django example - uni_data = University.objects.get(university_id) new_data = UniversitySerializer(uni_data[0], request_payload_data, partial=True) if new_data.is_valid(raise_exception=True): new_data.save()

Any similar Serializers approach is available in Pynamodb?

@ikonst

ikonst commented 1 year ago

PynamoDB is not set to support any particular web app framework. All we provide is an schema layer on top of DynamoDB.

If you don't need the schema, i.e. you just want to persist arbitrary JSON data, wouldn't you be better off using boto3 directly?

satyam8080 commented 1 year ago

Table: University { location:{ city: "New York", latitude: "40.7128° N", longitude: "74.0060° W" }, name: "MIT", other_fields:{ others: "random data" } }

If I want to update the city location from New York to California, I have to pass the complete location object otherwise rest fields will get removed and only the updated field will remain, is it possible to update only the city using pynamodb?

university.update(
actions=[UniversityModel.location.city.set("California")]
)

The above query will work but what if we don't know which field is getting updated, Is there any way to update it dynamically and without passing other fields of the location object?

ikonst commented 1 year ago

You can construct it dynamically with getattr.

satyam8080 commented 1 year ago

Hi @ikonst, can you please provide some examples of how to implement it?

ikonst commented 1 year ago

Something like this (untested...):

actions = []

def update_from_dict(m, d):
    for k, v in d.items():
        sub_m = getattr(m, k)
        if isinstance(v, dict):
            update_from_dict(v, sub_m)
        else:
            actions.append(sub_m.set(v))

update_from_dict(UniversityModel, my_request_dict)

university.update(actions=actions)

This wouldn't cover all bases, for example it won't handle the cases when you need to create a map that didn't exist before, but that's not unique to PynamoDB -- whenever you have an "update" operation, in any kind of system, you need to decide how to handle such cases.

satyam8080 commented 1 year ago

Thanks @ikonst, by modifying your suggested logic, I came up with a solution that handles some edge cases (like it won't handle the cases when you need to create a map that didn't exist before)

    try:
        uni_data = UniversityModel.get(university_id)
        campus = campus.dict(exclude_none=True, exclude_unset=True)

        for k, v in campus.items():
            if uni_data.campus is None:
                # Add new object when the campus is None
                uni_data.campus = {k: v}
            else:
                # Find the relevant key in campus and update it with appropriate value
                uni_data.campus[k] = v

        uni_data.updated_at = datetime.utcnow()
        uni_data.save()

        return uni_data.campus.attribute_values

    except UniversityModel.DoesNotExist:
        return JSONResponse(status_code=404, content={"message": f"Invalid university id"})

Please review it when you can

ikonst commented 1 year ago

For clarity, I would do the try-except around the uni_data = UniversityModel.get(university_id) line only.

But overall, yes, something like this should work.