-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42 from cloudblue/example_project
Example project added
- Loading branch information
Showing
34 changed files
with
1,337 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Copyright © 2021 Ingram Micro Inc. All rights reserved. |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
79 changes: 79 additions & 0 deletions
79
examples/demo_project/master_service/app/migrations/0001_initial.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
), | ||
] |
40 changes: 40 additions & 0 deletions
40
examples/demo_project/master_service/app/migrations/0002_fixtures.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
Oops, something went wrong.