django-denormal


Namedjango-denormal JSON
Version 1.3.3 PyPI version JSON
download
home_pageNone
SummaryDjango automatic denormalization toolkit
upload_time2024-03-21 06:11:02
maintainerNone
docs_urlNone
authortrashnroll
requires_pythonNone
licenseMIT
keywords django denormalization
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # django-denormal

**denormal** is a Django denormalization toolkit.

It provides a set of extra model fields to ease the implementation of some typical denormalization scenarios by eliminating handwritten boilerplate code.

denormal relies on standard Django signals machinery, so it's basically (excluding some contrib fields) db agnostic - just as Django ORM is.

## TL;DR

The following example gets your data denormalized with no boilerplate code.

```python
# models.py

import denormal
from django.db import models

class Comment(models.Model):
    post = models.ForeignKey('Post', on_delete=models.CASCADE)
    text = models.TextField()

class Post(models.Model):
    comment_count = denormal.CountField('comment_set')
    first_comment = denormal.RelationField(
        relation_name='comment_set', fields=('id', 'text'), limit=1,
        flat=True)

# tests.py

from django.test import TestCase
from .models import Post, Comment

class DummyTest(TestCase):

    def test_denormal(self):
        post = Post.objects.create()

        # let's add a comment for the blog post
        Comment.objects.create(post=post)

        # denormal magic happened, so we already have correct value for a
        # corresponding comment_count and first_comment fields in the db

        # now we just have to refresh post instance to get its fields
        # updated
        post.refresh_from_db()

        # TADA!
        self.assertEqual(post.comment_count, 1)
        self.assertEqual(post.first_comment, post.comment_set.first())
```

## How it works
denormal automatically creates and connects signal receivers with boring logic under the hood to handle almost every common case of related data modification as denormalized fields update trigger, except for ORM update statements, as they bypass signals entirely.

The only requirement for the augmented model (the one with denormal field added to hold denormalized value) is to have a standard Django relation descriptor, as it is internally used to reach the desired data source. You can use, for example, standard backward relation accessors, that are auto-created for relationship fields.

denormal currently supports Django 2.2-4.0 and recent versions of Python3.

Work on documentation and tests is in progress, any help would be appreciated.

## Fields
The following arguments are available to all field types:

relation_name - points to the denormalized data source accessor
qs_filter - takes a dict or Q with extra filtration parameters for the related data queryset

### CountField
Provides the actual related items count. A typical case would be, say, a number of comments for a blog post.

Has no extra params.

Example:
```python
class Comment(models.Model):
    post = models.ForeignKey('Post', on_delete=models.CASCADE)

class Post(models.Model):
    comment_count = denormal.CountField('comment_set')
```

There's one more, with qs filtration - that one will count only comments with is_deleted == False:

```python
class Comment(models.Model):
    post = models.ForeignKey('Post', on_delete=models.CASCADE)
    is_deleted = models.BooleanField(default=False)

class Post(models.Model):
    comment_count = denormal.CountField(
        relation_name='comment_set', qs_filter={'is_deleted': False})
```

### SumField
Supplies the actual sum of specific foreign model field values.

Extra params:

internal_type - internal field type, used to store and validate your data, e.g., IntegerField or DecimalField
field_name - name of the foreign model field, that holds collected values
Example:

```python
class Transaction(models.Model):
    account = models.ForeignKey(
        'Account', related_name='transactions', on_delete=models.CASCADE)
    amount = models.IntegerField(default=0)

class Account(models.Model):
    balance = denormal.SumField(
        relation_name='transactions', field_name='amount')
```

(obviously, this approach is not recommended for maintaining the actual account balance)

### AvgField
Maintains the actual average value of specific foreign model field values.

Extra params:

internal_type
field_name
Same as above, see SumField for details.

### RelationField
Stores serialized set of related foreign model instances (fk, m2m, generic fk - whatever you may need) - entire records or specific fields only. Appears/behaves just like evaluated queryset to the end user, however, it saves you some precious db hits.

Extra params:

fields - required list of serialized field names
limit - number of records to store
flat - use to unwrap the result list with a single item in it, requires limit=1
Example:

```python
class Comment(models.Model):
    post = models.ForeignKey('Post', on_delete=models.CASCADE)
    is_deleted = models.BooleanField(default=False)

class Post(models.Model):
    first_five_comments = denormal.RelationField(
        relation_name='comment_set',
        qs_filter={'is_deleted': False},
        limit=5)
```

Bang! This post's first_five_comments field now stores first 5 comments (as a list), and you can immediately use them with no extra db queries.

## Miscellaneous
### contrib.RelationValueSetField
Extracts and stores a set of foreign model single field values. Defaults to an empty list.
This field is available only with Postgres db backend, as it uses django.contrib.postgres.fields.ArrayField as a base class.

Extra params:

default=list - regular Django field default parameter, so it can be callable
field_name - a name of a foreign model field to collect its values
Example:

```python
class Comment(models.Model):
    post = models.ForeignKey('Post', on_delete=models.CASCADE)
    author_name = models.CharField(max_length=100)

class Post(models.Model):
    comment_author_names = RelationValueSetField(
        relation_name='comment_set',
        default=list,
        field_name='author_name')
```

### Custom fields
You can use denormaldFieldMixin to implement your own denormalized fields with custom data extraction logic. See the source code for examples.


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "django-denormal",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": "django denormalization",
    "author": "trashnroll",
    "author_email": "trashnroll@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/f7/18/30c2e089d00d6ad9baf3bb7ccdaf685c1ae85bc114267935d0e309cd97cd/django-denormal-1.3.3.tar.gz",
    "platform": null,
    "description": "# django-denormal\n\n**denormal** is a Django denormalization toolkit.\n\nIt provides a set of extra model fields to ease the implementation of some typical denormalization scenarios by eliminating handwritten boilerplate code.\n\ndenormal relies on standard Django signals machinery, so it's basically (excluding some contrib fields) db agnostic - just as Django ORM is.\n\n## TL;DR\n\nThe following example gets your data denormalized with no boilerplate code.\n\n```python\n# models.py\n\nimport denormal\nfrom django.db import models\n\nclass Comment(models.Model):\n    post = models.ForeignKey('Post', on_delete=models.CASCADE)\n    text = models.TextField()\n\nclass Post(models.Model):\n    comment_count = denormal.CountField('comment_set')\n    first_comment = denormal.RelationField(\n        relation_name='comment_set', fields=('id', 'text'), limit=1,\n        flat=True)\n\n# tests.py\n\nfrom django.test import TestCase\nfrom .models import Post, Comment\n\nclass DummyTest(TestCase):\n\n    def test_denormal(self):\n        post = Post.objects.create()\n\n        # let's add a comment for the blog post\n        Comment.objects.create(post=post)\n\n        # denormal magic happened, so we already have correct value for a\n        # corresponding comment_count and first_comment fields in the db\n\n        # now we just have to refresh post instance to get its fields\n        # updated\n        post.refresh_from_db()\n\n        # TADA!\n        self.assertEqual(post.comment_count, 1)\n        self.assertEqual(post.first_comment, post.comment_set.first())\n```\n\n## How it works\ndenormal automatically creates and connects signal receivers with boring logic under the hood to handle almost every common case of related data modification as denormalized fields update trigger, except for ORM update statements, as they bypass signals entirely.\n\nThe only requirement for the augmented model (the one with denormal field added to hold denormalized value) is to have a standard Django relation descriptor, as it is internally used to reach the desired data source. You can use, for example, standard backward relation accessors, that are auto-created for relationship fields.\n\ndenormal currently supports Django 2.2-4.0 and recent versions of Python3.\n\nWork on documentation and tests is in progress, any help would be appreciated.\n\n## Fields\nThe following arguments are available to all field types:\n\nrelation_name - points to the denormalized data source accessor\nqs_filter - takes a dict or Q with extra filtration parameters for the related data queryset\n\n### CountField\nProvides the actual related items count. A typical case would be, say, a number of comments for a blog post.\n\nHas no extra params.\n\nExample:\n```python\nclass Comment(models.Model):\n    post = models.ForeignKey('Post', on_delete=models.CASCADE)\n\nclass Post(models.Model):\n    comment_count = denormal.CountField('comment_set')\n```\n\nThere's one more, with qs filtration - that one will count only comments with is_deleted == False:\n\n```python\nclass Comment(models.Model):\n    post = models.ForeignKey('Post', on_delete=models.CASCADE)\n    is_deleted = models.BooleanField(default=False)\n\nclass Post(models.Model):\n    comment_count = denormal.CountField(\n        relation_name='comment_set', qs_filter={'is_deleted': False})\n```\n\n### SumField\nSupplies the actual sum of specific foreign model field values.\n\nExtra params:\n\ninternal_type - internal field type, used to store and validate your data, e.g., IntegerField or DecimalField\nfield_name - name of the foreign model field, that holds collected values\nExample:\n\n```python\nclass Transaction(models.Model):\n    account = models.ForeignKey(\n        'Account', related_name='transactions', on_delete=models.CASCADE)\n    amount = models.IntegerField(default=0)\n\nclass Account(models.Model):\n    balance = denormal.SumField(\n        relation_name='transactions', field_name='amount')\n```\n\n(obviously, this approach is not recommended for maintaining the actual account balance)\n\n### AvgField\nMaintains the actual average value of specific foreign model field values.\n\nExtra params:\n\ninternal_type\nfield_name\nSame as above, see SumField for details.\n\n### RelationField\nStores serialized set of related foreign model instances (fk, m2m, generic fk - whatever you may need) - entire records or specific fields only. Appears/behaves just like evaluated queryset to the end user, however, it saves you some precious db hits.\n\nExtra params:\n\nfields - required list of serialized field names\nlimit - number of records to store\nflat - use to unwrap the result list with a single item in it, requires limit=1\nExample:\n\n```python\nclass Comment(models.Model):\n    post = models.ForeignKey('Post', on_delete=models.CASCADE)\n    is_deleted = models.BooleanField(default=False)\n\nclass Post(models.Model):\n    first_five_comments = denormal.RelationField(\n        relation_name='comment_set',\n        qs_filter={'is_deleted': False},\n        limit=5)\n```\n\nBang! This post's first_five_comments field now stores first 5 comments (as a list), and you can immediately use them with no extra db queries.\n\n## Miscellaneous\n### contrib.RelationValueSetField\nExtracts and stores a set of foreign model single field values. Defaults to an empty list.\nThis field is available only with Postgres db backend, as it uses django.contrib.postgres.fields.ArrayField as a base class.\n\nExtra params:\n\ndefault=list - regular Django field default parameter, so it can be callable\nfield_name - a name of a foreign model field to collect its values\nExample:\n\n```python\nclass Comment(models.Model):\n    post = models.ForeignKey('Post', on_delete=models.CASCADE)\n    author_name = models.CharField(max_length=100)\n\nclass Post(models.Model):\n    comment_author_names = RelationValueSetField(\n        relation_name='comment_set',\n        default=list,\n        field_name='author_name')\n```\n\n### Custom fields\nYou can use denormaldFieldMixin to implement your own denormalized fields with custom data extraction logic. See the source code for examples.\n\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Django automatic denormalization toolkit",
    "version": "1.3.3",
    "project_urls": null,
    "split_keywords": [
        "django",
        "denormalization"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "f71830c2e089d00d6ad9baf3bb7ccdaf685c1ae85bc114267935d0e309cd97cd",
                "md5": "91c08b3492c95ebcd926fe6e09d27406",
                "sha256": "58d0569cb2737c18345cf56c20baac876d53034417dc0cbbecd31b38eba7fa89"
            },
            "downloads": -1,
            "filename": "django-denormal-1.3.3.tar.gz",
            "has_sig": false,
            "md5_digest": "91c08b3492c95ebcd926fe6e09d27406",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 20991,
            "upload_time": "2024-03-21T06:11:02",
            "upload_time_iso_8601": "2024-03-21T06:11:02.281240Z",
            "url": "https://files.pythonhosted.org/packages/f7/18/30c2e089d00d6ad9baf3bb7ccdaf685c1ae85bc114267935d0e309cd97cd/django-denormal-1.3.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-21 06:11:02",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "django-denormal"
}
        
Elapsed time: 0.36481s