Skip to content

Commit

Permalink
Merge pull request #42 from cloudblue/example_project
Browse files Browse the repository at this point in the history
Example project added
  • Loading branch information
maxipavlovic authored Jul 26, 2021
2 parents d401819 + a114e43 commit 561715d
Show file tree
Hide file tree
Showing 34 changed files with 1,337 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Full documentation is available at [https://django-cqrs.readthedocs.org](https:/
Examples
========

You can find an example project [here](examples/demo_project/README.md)

Integration
-----------
* Setup `RabbitMQ`
Expand Down
47 changes: 47 additions & 0 deletions examples/demo_project/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# CQRS demo project

It's a simple demo project contains 2 services:

- master: source of domain models. Stores models in PostgreSQL.
- replica: service which get models from master by CQRS. Stores replicated models in MySQL and Redis

## Start project:

```
docker-compose up -d db_pgsql db_mysql
docker-compose run master ./manage.py migrate
docker-compose run replica ./manage.py migrate
docker-compose up -d
docker-compose run master ./manage.py cqrs_sync --cqrs-id=user -f={}
docker-compose run master ./manage.py cqrs_sync --cqrs-id=product -f={}
```

It starts master WEB app on [http://127.0.0.1:8000](http://127.0.0.1:8000) and replica on [http://127.0.0.1:8001](http://127.0.0.1:8001)

You can do something with model instances via WEB interface or django shell on master and see how data changes in replica too.


## Domain models:

### User:

The most common and simple way for replication is used for this model.

### ProductType:

This model isn't being synchronized separately, only with related Product.

### Product:

This models uses custom own written serializer and relation optimization.

### Purchase:

This models uses Django REST Framework serializer. Replica service stores this model in redis.


## Monitoring

You can monitor CQRS queue by tools provided by chosen transport backend.

For this demo we use RabbitMQ with management plugin. You can find it on [http://127.0.0.1:15672](http://127.0.0.1:15672) with credentials `rabbitmq / password`.
95 changes: 95 additions & 0 deletions examples/demo_project/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
version: "3"


services:
# database for master
db_pgsql:
image: postgres:12
environment:
POSTGRES_USER: master_service
POSTGRES_PASSWORD: password
POSTGRES_DB: master_service
volumes:
- pgsql_data:/var/lib/postgresql/data

# database for replica
db_mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: replica_service
MYSQL_PASSWORD: password
MYSQL_DATABASE: replica_service
volumes:
- mysql_data:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password

# cache storage for replica
redis:
image: redis

# CQRS transport backend
rabbitmq:
image: rabbitmq:3-management-alpine
environment:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: password
ports:
- 15672:15672

# Domain models provider
master:
build:
context: master_service
ports:
- 8000:8000
depends_on:
- db_pgsql
- rabbitmq
volumes:
- ./master_service:/app
command: >
dockerize -wait tcp://rabbitmq:5672 -timeout 30s
dockerize -wait tcp://db_pgsql:5432 -timeout 30s
./manage.py runserver 0.0.0.0:8000
# replica WEB app
replica:
build:
context: replica_service
ports:
- 8001:8000
depends_on:
- db_mysql
- redis
- rabbitmq
volumes:
- ./replica_service:/app
command: >
dockerize -wait tcp://rabbitmq:5672 -timeout 30s
dockerize -wait tcp://db_mysql:3306 -timeout 30s
dockerize -wait tcp://redis:6379 -timeout 30s
./manage.py runserver 0.0.0.0:8000
# replica CQRS consumer worker
replica_cqrs_consumer:
build:
context: replica_service
depends_on:
- db_mysql
- rabbitmq
volumes:
- ./replica_service:/app
command: >
dockerize -wait tcp://rabbitmq:5672 -timeout 30s
dockerize -wait tcp://db_mysql:3306 -timeout 30s
dockerize -wait tcp://redis:6379 -timeout 30s
./manage.py cqrs_consume -w2
volumes:
pgsql_data:
driver: local

mysql_data:
driver: local
16 changes: 16 additions & 0 deletions examples/demo_project/master_service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3

WORKDIR /app

COPY requirements.txt .

RUN python -mpip install -r requirements.txt

ENV DOCKERIZE_VERSION v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz

COPY . .

CMD ./manage.py runserver 0.0.0.0:8000
1 change: 1 addition & 0 deletions examples/demo_project/master_service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
Empty file.
18 changes: 18 additions & 0 deletions examples/demo_project/master_service/app/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright © 2021 Ingram Micro Inc. All rights reserved.

"""
ASGI config for master_service project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'master_service.settings')

application = get_asgi_application()
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright © 2021 Ingram Micro Inc. All rights reserved.

from django.conf import settings
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('cqrs_revision', models.IntegerField(default=0, help_text="This field must be incremented on any model update. It's used to for CQRS sync.")),
('cqrs_updated', models.DateTimeField(auto_now=True, help_text="This field must be incremented on every model update. It's used to for CQRS sync.")),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cqrs_revision', models.IntegerField(default=0, help_text="This field must be incremented on any model update. It's used to for CQRS sync.")),
('cqrs_updated', models.DateTimeField(auto_now=True, help_text="This field must be incremented on every model update. It's used to for CQRS sync.")),
('name', models.CharField(max_length=50)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ProductType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='Purchase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cqrs_revision', models.IntegerField(default=0, help_text="This field must be incremented on any model update. It's used to for CQRS sync.")),
('cqrs_updated', models.DateTimeField(auto_now=True, help_text="This field must be incremented on every model update. It's used to for CQRS sync.")),
('action_time', models.DateTimeField(auto_now_add=True)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.product')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='product',
name='product_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.producttype'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright © 2021 Ingram Micro Inc. All rights reserved.

from django.db import migrations


def create_users(apps, schema_editor):
User = apps.get_model('app', 'User')
to_create = []
for username in ('Mal', 'Zoe', 'Wash', 'Inara', 'Jayne', 'Kaylee', 'Simon', 'River'):
to_create.append(User(username=username))
User.objects.bulk_create(to_create)


def create_products(apps, schema_editor):
ProductType = apps.get_model('app', 'ProductType')
Product = apps.get_model('app', 'Product')

products = {
'food': ['apple', 'meat', 'banana'],
'weapon': ['blaster', 'gun', 'knife'],
'starships': ['Serenity'],
}
to_create = []
for key, items in products.items():
product_type = ProductType.objects.create(name=key)
for product in items:
to_create.append(Product(name=product, product_type=product_type))
Product.objects.bulk_create(to_create)


class Migration(migrations.Migration):

dependencies = [
('app', '0001_initial'),
]

operations = [
migrations.RunPython(create_users, migrations.RunPython.noop),
migrations.RunPython(create_products, migrations.RunPython.noop),
]
Empty file.
39 changes: 39 additions & 0 deletions examples/demo_project/master_service/app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
from dj_cqrs.mixins import MasterMixin

from django.contrib.auth.models import AbstractUser
from django.db import models


class User(MasterMixin, AbstractUser):
CQRS_ID = 'user'
CQRS_PRODUCE = True


class ProductType(models.Model):
name = models.CharField(max_length=50)


class Product(MasterMixin, models.Model):
CQRS_ID = 'product'
CQRS_SERIALIZER = 'app.serializers.ProductSerializer'

name = models.CharField(max_length=50)
product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE)

@classmethod
def relate_cqrs_serialization(cls, queryset):
return queryset.select_related('product_type')


class Purchase(MasterMixin, models.Model):
CQRS_ID = 'purchase'
CQRS_SERIALIZER = 'app.serializers.PurchaseSerializer'

user = models.ForeignKey(User, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
action_time = models.DateTimeField(auto_now_add=True)

@classmethod
def relate_cqrs_serialization(cls, queryset):
return queryset.select_related('product', 'product__product_type')
34 changes: 34 additions & 0 deletions examples/demo_project/master_service/app/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
from rest_framework import serializers

from app.models import Purchase


class ProductSerializer:
"""
Simple serializer
"""
def __init__(self, instance):
self.instance = instance

@property
def data(self):
return {
'id': self.instance.id,
'name': self.instance.name,
'product_type': {
'id': self.instance.product_type.id,
'name': self.instance.product_type.name,
},
}


class PurchaseSerializer(serializers.ModelSerializer):
"""
Django REST Framework serializers are compatible
"""
product_name = serializers.CharField(source='product.name')

class Meta:
model = Purchase
fields = ('id', 'user_id', 'product_name', 'action_time')
Loading

0 comments on commit 561715d

Please sign in to comment.