Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change "object_id" to be a "CharField" #138

Merged
merged 8 commits into from
Jul 22, 2020

Conversation

jsoa
Copy link
Contributor

@jsoa jsoa commented May 14, 2020

The code in this change set migrates the current CrudEvent.object_id from an IntegerField to a CharField as a means to allow various objects with different PK types.

This includes a change to the model_signals that query's for the object instead of looking for the PK, this is because the common UUIDField as a PK usually has a default, thereofre PK would never be empty, even on creation.

This also includes a bug fix for post_delete with the PK not being available in the crud_flow closure. See #138 (comment)

Fixes: #131
Fixes: #90

  • Tested with Django 3.0
  • Tested with UUIDField
  • Tested with BigAutoField
  • Added Unit tests for UUIDField and BigAutoField
  • Added Unit tests for deleting objects
  • Added Test models for UUIDField and BigAutoField
  • Added test app endpoints for UUID and BigInt
  • Includes updates to the sqlite DB with data related UUID and BigInt

Deep dive

The main purpose of these tests is to ensure current users of this app, will not need worry about the migration that will change the type of object_id from integer to char. All the output below should be the same per test group, i.e. django version and db. If anything should be different, it will be noted below.

All the test below will do the following

  1. Checkout current master and initialize the test project
  2. Hit test_app/create_obj/ 5 times
  3. Hit test_app/update_obj/?id=N for the 5 items above
  4. print the following to verify the current type of object is int and basic query works
>>> for x in CRUDEvent.objects.all():
...     print(x.content_type, x.object_id, type(x.object_id))
...
test_app | test model 5 <class 'int'>
test_app | test model 4 <class 'int'>
test_app | test model 3 <class 'int'>
test_app | test model 2 <class 'int'>
test_app | test model 1 <class 'int'>
test_app | test model 5 <class 'int'>
test_app | test model 4 <class 'int'>
test_app | test model 3 <class 'int'>
test_app | test model 2 <class 'int'>
test_app | test model 1 <class 'int'>
 >>> CRUDEvent.objects.filter(object_id=int(1))
<QuerySet [<CRUDEvent: CRUDEvent object (6)>, <CRUDEvent: CRUDEvent object (1)>]>
  1. checkout the new branch and run migrations
  2. print the same output as above, ensuring the new type is str and the query still works and returns the same results, by passing it str and int versions.
>>> for x in CRUDEvent.objects.all():
...     print(x.content_type, x.object_id, type(x.object_id))
...
test_app | test model 5 <class 'str'>
test_app | test model 4 <class 'str'>
test_app | test model 3 <class 'str'>
test_app | test model 2 <class 'str'>
test_app | test model 1 <class 'str'>
test_app | test model 5 <class 'str'>
test_app | test model 4 <class 'str'>
test_app | test model 3 <class 'str'>
test_app | test model 2 <class 'str'>
test_app | test model 1 <class 'str'>
>>> CRUDEvent.objects.filter(object_id='1')
<QuerySet [<CRUDEvent: CRUDEvent object (6)>, <CRUDEvent: CRUDEvent object (1)>]>
>>> CRUDEvent.objects.filter(object_id=int(1))
<QuerySet [<CRUDEvent: CRUDEvent object (6)>, <CRUDEvent: CRUDEvent object (1)>]>

Current progress

Django Version DB DB Version
[x] 3.0 Postgres 12.0
[x] 3.0 Postgres 11.0
[x] 3.0 Postgres 10.0
[x] 3.0 Postgres 9
[x] 2.2 Postgres 12.0
[x] 2.2 Postgres 11.0
[x] 2.2 Postgres 10.0
[x] 2.2 Postgres 9.0
[x]* 1.11 Postgres 12.0
[x]* 1.11 Postgres 11.0
[x]* 1.11 Postgres 10.0
[x]* 1.11 Postgres 9.0
[x] 3.0 MySQL 8
[x] 3.0 MySQL 5
[x] 2.2 MySQL 8
[x] 2.2 MySQL 5
[x]* 1.11 MySQL 8
[x]* 1.11 MySQL 5

Notes

  • Django 1.11 created more CRUDEvent objects (on master branch)
>>> for x in CRUDEvent.objects.all():
...     print(x.content_type, x.object_id, type(x.object_id))
...
test model 5 <class 'int'>
test model 4 <class 'int'>
test model 3 <class 'int'>
test model 2 <class 'int'>
test model 1 <class 'int'>
test model 5 <class 'int'>
test model 4 <class 'int'>
test model 3 <class 'int'>
test model 2 <class 'int'>
test model 1 <class 'int'>
content type 12 <class 'int'>
content type 11 <class 'int'>
content type 10 <class 'int'>
content type 9 <class 'int'>
content type 8 <class 'int'>
content type 7 <class 'int'>
content type 6 <class 'int'>
content type 5 <class 'int'>
content type 4 <class 'int'>
content type 3 <class 'int'>
content type 1 <class 'int'>
  • After migrating
content type 18 <class 'str'>
content type 17 <class 'str'>
content type 16 <class 'str'>
content type 15 <class 'str'>
content type 14 <class 'str'>
content type 13 <class 'str'>
test model 5 <class 'str'>
test model 4 <class 'str'>
test model 3 <class 'str'>
test model 2 <class 'str'>
test model 1 <class 'str'>
test model 5 <class 'str'>
test model 4 <class 'str'>
test model 3 <class 'str'>
test model 2 <class 'str'>
test model 1 <class 'str'>
content type 12 <class 'str'>
content type 11 <class 'str'>
content type 10 <class 'str'>
content type 9 <class 'str'>
content type 8 <class 'str'>
content type 7 <class 'str'>
content type 6 <class 'str'>
content type 5 <class 'str'>
content type 4 <class 'str'>
content type 3 <class 'str'>
content type 1 <class 'str'>

Copy link
Contributor

@steverecio steverecio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Just needs one small fix (change id to pk)

else:
# Query the object, see if it exists
try:
old_model = sender.objects.get(id=instance.pk)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change this to pk=instance.pk

I have primary keys not named id so this throws an exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steverecio updated

Copy link
Contributor

@steverecio steverecio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - I have this code deployed on my project and it seems to work as intended.

@steverecio
Copy link
Contributor

One note after testing this a bit more. The object_id should have null=True to handle Delete events. I'm seeing the following error after deleting objects:

easy audit had a post_delete exception on CRUDEvent creation
...
IntegrityError: null value in column "object_id" violates not-null constraint

@jsoa
Copy link
Contributor Author

jsoa commented May 18, 2020

@steverecio I was seeing this as well with postgres, doesn't seem to be an issue with sqlite.

The instance id is stripped before the transaction completes with latest postgres. Simple fix i think.

@jsoa
Copy link
Contributor Author

jsoa commented May 18, 2020

Last commit should address the issue without needing to make the field nullable, here is a screenshot of a project ive been testing with..
2020-05-17_20:10:15

@steverecio
Copy link
Contributor

@jsoa nice fix! I like your approach much better.

@jheld @soynatan can we get this merged in?

@jsoa
Copy link
Contributor Author

jsoa commented May 28, 2020

@Etenil @jheld Updated code in order to not do the explicit query in the pre_save signal. Instead it will use the internal _state object to determine if the object is being added. Took a look and _state has been around since django 1.2

@jheld
Copy link
Collaborator

jheld commented May 28, 2020

Oh that's a nice idea.

We use the _state a little in my project (I don't love it -- I wish django would make the contract exposed/official). I think so long as our test case is written such that if that goes away/changes, then it'll fail and then we'll be aware before release.

I/we need to first fix/address the breakage on the request event user signal before merging anything else. Then we can add a couple more things (nothing which breaks compatibility/super risky).

This seems like a good candidate!

@jsoa
Copy link
Contributor Author

jsoa commented May 29, 2020

@jheld I also don't love checking _state, however I did add a compatibility test to ensure its available.

@jheld
Copy link
Collaborator

jheld commented May 29, 2020

Excellent work with all of the test cases and test models. Comprehensive and pretty modular/DRY.

This looks like a great candidate for 1.3.0. I hope to finalize 1.2.3 this or next week.

@jheld
Copy link
Collaborator

jheld commented Jun 4, 2020

Likely will release 1.2.3 this week/early next week. There was a bug introduced in the request event handler at the beginning of the alphas and the last/current alpha (4) effectively rolls back that change.

@jheld
Copy link
Collaborator

jheld commented Jul 1, 2020

About ready to merge this!

@jsoa

So TestModel is still implicitly using AutoField (e.g. regular INT), right? And that is the "base" test case model so it is tested, and that's good.

Can you take a look over the comments/reviews once more to make sure all of those are answered/moot?

@jsoa
Copy link
Contributor Author

jsoa commented Jul 2, 2020

@jheld That is correct TestModel is the basic django model with AutoField (integer PK)

I took a look again and everything seems good to go. The only thing that i'm not 100% certain of is if existing users of this app will have to do anything special after the schema migration is run, such as converting the integers to chars.

Copy link
Collaborator

@jheld jheld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the sqlite test db file?

@jheld
Copy link
Collaborator

jheld commented Jul 11, 2020

@jsoa if I'm understanding your question, so long as they run migrations, they shouldn't have to do anything other than that. I'm happy to test on my local, too, next week. My project runs on the default configuration.

@pcraig3
Copy link

pcraig3 commented Jul 17, 2020

Hello! Really excited to see this PR go in.

I'm working on a Django project where we're using UUIDs as primary keys for our users and this PR resolves the issues I'm seeing. It seems like it's nearly there, so hope this can be merged soon! 👍

@jsoa
Copy link
Contributor Author

jsoa commented Jul 21, 2020

@jheld started testing the migration (manually). See PR description for details, still work in progress. we can add more DB versions and/or django versions if needed.

@jheld
Copy link
Collaborator

jheld commented Jul 21, 2020

@jsoa this looks really great. Thank you for running through some simple (but time consuming!) test cases. My project uses MySQL but if you think you'll have time to run through that on your end I'll wait til then. Let me know -- happy to follow the instructions you've laid out.

@jsoa
Copy link
Contributor Author

jsoa commented Jul 21, 2020

@jheld done with my testing, everything seems good to go, with the minor note about django 1.11. It would be nice to get someone to run through just some of the manual tests that was run.

Here is a docker-compose file i used for bootstrapping the DBs

version: "3"

services:

  # POSTGRES
  ea-psql12:
    image: postgres:12.0

    environment:
      - POSTGRES_PASSWORD=ea
      - POSTGRES_USER=ea
      - POSTGRES_DB=ea

    ports:
      - "54320:5432"

  ea-psql11:
    image: postgres:11.0

    environment:
      - POSTGRES_PASSWORD=ea
      - POSTGRES_USER=ea
      - POSTGRES_DB=ea

    ports:
      - "54321:5432"


  ea-psql10:
    image: postgres:10.0

    environment:
      - POSTGRES_PASSWORD=ea
      - POSTGRES_USER=ea
      - POSTGRES_DB=ea

    ports:
      - "54322:5432"

  ea-psql9:
    image: postgres:9

    environment:
      - POSTGRES_PASSWORD=ea
      - POSTGRES_USER=ea
      - POSTGRES_DB=ea

    ports:
      - "54323:5432"


  # MYSQL
  ea-mysql8:
    image: mysql:8

    environment:
      - MYSQL_ROOT_PASSWORD=ea
      - MYSQL_DATABASE=ea
      - MYSQL_USER=ea
      - MYSQL_PASSWORD=ea

    ports:
      - "33060:3306"

  ea-mysql5:
    image: mysql:5

    environment:
      - MYSQL_ROOT_PASSWORD=ea
      - MYSQL_DATABASE=ea
      - MYSQL_USER=ea
      - MYSQL_PASSWORD=ea

    ports:
      - "33061:3306"

Each DB has a different host port

  • PG
    • 54320 - 12
    • 54321 - 11
    • 54322 - 10
    • 54323 - 9
  • MYSQL
    • 33060 - 8
    • 33061 - 5

After each django version check, we need to reset the containers, by docker rm <container id>
Then docker volume rm $(docker volume ls --quiet --filter dangling=true) || true (removes any volume not attached to a container)
Then its just manually changed the DB connection in the test_project per DB version and Django version.

    # 'default': {
    #     'ENGINE': 'django.db.backends.postgresql',
    #     'NAME': 'ea',
    #     'USER': 'ea',
    #     'PASSWORD': 'ea',
    #     'HOST': 'localhost',
    #     'PORT': '54323'
    # }

    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'ea',
        'USER': 'ea',
        'PASSWORD': 'ea',
        'HOST': '127.0.0.1',
        'PORT': '33061'
    }

@jheld
Copy link
Collaborator

jheld commented Jul 21, 2020

@jsoa excellent use for docker-compose.

We don't support django 1.11 officially anymore.

2.0+ (and realistically other than travis tests passing, we really only need to be 2.2+).

So while I agree 1.11 is odd, I will argue we should ignore the difference. I have no idea what has changed/subtly broken in our usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Presave signal fails when using UUID field as PK Charfield Primary Key
5 participants