SlideShare a Scribd company logo
Strategies for refactoring and migrating a 
big old project to be multilingual and use 
multiple databases or how I learned to stop 
worrying and search and replace my code 
base. 
8th Django Copenhagen Meetup 
Benjamin Bach 
benjamin@overtag.dk
Thanks! 
Title backup responsible: 
@valberg
1 
The project “toughroad” 
2 
multiple database schemas 
3 
multilingual (django-parler)
The project
A game: 
80-250 players 
Educational (Global trade issues) 
~6 hours 
Physical role play + 
computer interactions
Illustration of a global trade chain: 
Farmers 
Traders 
Exporters 
Banks 
Brokers Brand companies 
Cafés 
 

Strategies for refactoring and migrating a big old project to be multilingual and use multiple databases or how I learned to stop worrying and search and replace my code base.
Strategies for refactoring and migrating a big old project to be multilingual and use multiple databases or how I learned to stop worrying and search and replace my code base.
Strategies for refactoring and migrating a big old project to be multilingual and use multiple databases or how I learned to stop worrying and search and replace my code base.
Testability 
Unfeasible: 
Either get 80 people or simulate 
80 people's interactions 
Even worse: Every role is unique and there 
are up to 250+ 
Instruction manuals, interdependent 
human behavior, human errors are 
part of the game.
2009: First games played. 
2010: First successful game. 
Fixing issues during gameplay for 
~2 years 
Summer 2014: Game has worked 
flawlessly for a couple of years.
http://cloc.sourceforge.net v 1.60 T=2.09 s (89.9 files/s, 13062.9 lines/s) 
------------------------------------------------------------------------------- 
Language files blank comment code 
------------------------------------------------------------------------------- 
Python 81 2948 1062 12832 
HTML 100 1519 2 7326 
CSS 3 133 31 953 
Javascript 3 43 2 443 
Bourne Shell 1 4 1 8 
------------------------------------------------------------------------------- 
SUM: 188 4647 1098 21562 
------------------------------------------------------------------------------- 
(september 2014)
Finally! Success! New partners, more 
attention, new problems. 
Does it translate to other countries? 
Does it scale?
1. Copying the game 
Old model: For every game, a new database. 
Each game shares copies “start-up” 
configuration from a prototype game. 
(MySQL)
New model: 
Use Postgres schemas! 
Now we can deploy each game inside its own 
schema and access shared data from the 
“public” schema.
Schemas 
Namespaces inside a database 
Like adding 
set(A1,B1) + set(A2,C2) = set(A1,B1,C2)
Example: 
Database:Google 
auth_userpublic + docsdocs + spreadsheetspreadsheet
Scalability and performance win! 
Manage large sets of data separately 
Share tables only where necessary 
Reduce use of managers
Downside: 
Hard to share data 
Makes migrations harder!
Using schemas is a fundamental 
Design decision!
Step 1: Setting up the project... 
1. settings.DATABASES 
2. settings.DATABASE_ROUTERS
DATABASES = { 
'default': { 
'ENGINE': 'django.db.backends.postgresql_psycopg2', 
'NAME': 'toughroad_dk', 
'USER': 'django', 
'PASSWORD': 'django', 
'HOST': '127.0.0.1', 
'OPTIONS': { 
'options': '-c search_path=public,' + os.environ['TOUGHROAD_SCHEMA'] 
}, 
}, 
'toughroad': { 
'ENGINE': 'django.db.backends.postgresql_psycopg2', 
'NAME': 'toughroad_dk', 
'USER': 'django', 
'PASSWORD': 'django', 
'HOST': '127.0.0.1', 
'OPTIONS': { 
'options': '-c search_path=' + os.environ['TOUGHROAD_SCHEMA'] + ',public' 
}, 
}, 
} 
settings.DATABASES
settings.DATABASE_ROUTERS 
DATABASE_ROUTERS = ['toughroad.database_routers.ToughroadRouter']
APPS = ('toughroad', 'sessions') 
class ToughroadRouter(object): 
""" 
Put all game-specific data in a seperate database 
""" 
def db_for_read(self, model, **hints): 
if model._meta.app_label in APPS: 
return 'toughroad' 
return None 
def db_for_write(self, model, **hints): 
if model._meta.app_label in APPS: 
return 'toughroad' 
return None 
def allow_syncdb(self, db, model): 
if db == 'toughroad': 
return model._meta.app_label in APPS 
elif model._meta.app_label in APPS: 
return False 
return None 
def allow_relation(self, obj1, obj2, **hints): 
""" 
Allow relations if a model in the auth app is involved. 
""" 
if obj1._meta.app_label == 'default' or  
obj2._meta.app_label == 'toughroad': 
return True 
return None
# Specifying the DB from command line: 
$ python manage.py shell –database=toughroad 
# Using env stuff 
$ TOUGHROAD_SCHEMA='customer_A' python manage.py shell –database=toughroad
But why have different databases? 
Storing session data 
(south_)migration_history!
Step 2: 
Deployment. No fun using schemas 
if they're not automatically handled
manage.py deploy_game 
for game in games.filter(db_created=False): 
print "Creating schema for", game 
cursor = connection.cursor() 
cursor.execute("CREATE SCHEMA "" + game.schema_name + """) 
transaction.commit() 
...
manage.py deploy_game (2) 
for game in games.filter(db_created=False): 
... 
# syncdb for applications that do not have migrations and 
# are not in the public schema already 
env = {} 
env.update(os.environ.copy()) 
env.update(game.env) 
p = subprocess.Popen( 
['python', 'manage.py', 'syncdb', '--settings=settings.from_env', '--database=toughroad', '--noinput', '-- 
traceback'], 
env=env 
) 
p.communicate() 
if p.returncode != 0: 
raise RuntimeError("Tried command, it failed") 
...
manage.py deploy_game (3) 
for game in games.filter(db_created=False): 
... 
if trmeta_settings.BEFORE_MIGRATE: 
p = subprocess.Popen( 
shlex.split(trmeta_settings.BEFORE_MIGRATE), 
env=env 
) 
p.communicate() 
if p.returncode != 0: 
raise RuntimeError("Tried command, it failed") 
...
manage.py deploy_game (4) 
for game in games.filter(db_created=False): 
... 
if trmeta_settings.BEFORE_MIGRATE: 
p = subprocess.Popen( 
shlex.split(trmeta_settings.BEFORE_MIGRATE), 
env=env 
) 
p.communicate() 
if p.returncode != 0: 
raise RuntimeError("Tried command, it failed") 
...
manage.py deploy_game (5) 
for game in games.filter(db_created=False): 
... 
p = subprocess.Popen( 
['python', 'manage.py', 'deploy_game', 'south', '--settings=settings.from_env', '--traceback'], 
env=env, 
) 
p.communicate() 
if p.returncode != 0: 
raise RuntimeError("Tried command, it failed") 
game.db_created = True 
game.save() 
transaction.commit() 
...
manage.py deploy_game south 
... 
from django.db import connections, connection 
if options['south']: 
cursor = connections['toughroad_explicit'].cursor() 
cursor.execute("""CREATE TABLE "{:s}"."south_migrationhistory" ( 
"id" serial NOT NULL PRIMARY KEY, 
"app_name" varchar(255) NOT NULL, 
"migration" varchar(255) NOT NULL, 
"applied" timestamp with time zone NOT NULL 
) 
""".format(os.environ['TOUGHROAD_SCHEMA'])) 
transaction.commit_unless_managed(using="toughroad_explicit") 
cursor.close()
manage.py deploy_game (6) 
for game in games.filter(db_created=False): 
... 
print "Now migrating", game.schema_name 
env = {} 
env.update(os.environ.copy()) 
env.update(game.env) 
p = subprocess.Popen( 
['python', 'manage.py', 'migrate', 'toughroad', '--settings=settings.from_env', '--database=toughroad', 
'--no-initial-data', '--noinput', '--traceback'], 
env=env 
) 
p.communicate() 
if p.returncode != 0: 
raise RuntimeError("Tried command, it failed") 
...
manage.py deploy_game (7) 
for game in games.filter(db_created=False): 
... 
if trmeta_settings.AFTER_MIGRATE: 
p = subprocess.Popen( 
shlex.split(trmeta_settings.AFTER_MIGRATE), 
env=env 
) 
p.communicate() 
if p.returncode != 0: 
raise RuntimeError("Tried command, it failed") 
...
And the refactoring?
Splitting into separate applications! 
toughroad 
toughroad_meta
“toughroad” before 
class GameRound(models.Model): 
""" 
A simulated year. Managed by "gameloop", all other threads 
are notified of round changes. 
""" 
number = models.IntegerField( 
_('round number'), 
unique=True, 
help_text=_('Counts from 1!')) 
template_duration = models.IntegerField( 
_('duration'), 
default=30, 
) 
economic_growth = models.FloatField( 
default=1.0, verbose_name=_('economic growth'), 
help_text=_( 
'Economic growth factor (>0.0). 1.0=no growth. 1.1 = 10%% growth etc.')) 
is_started = models.BooleanField(default=False) 
started_on = models.DateTimeField(null=True, blank=True)
“toughroad” after 
from toughroad_meta.models import GameRound as GameRoundMeta 
class GameRound(GameRoundMeta): 
is_started = models.BooleanField(default=False) 
started_on = models.DateTimeField(null=True, blank=True)
“toughroad_meta” 
class GameRound(models.Model): 
""" 
A simulated year. Managed by "gameloop", all other threads 
are notified of round changes. 
""" 
number = models.IntegerField( 
_('round number'), 
unique=True, 
help_text=_('Counts from 1!')) 
template_duration = models.IntegerField( 
_('duration'), 
default=30, 
) 
economic_growth = models.FloatField( 
default=1.0, verbose_name=_('economic growth'), 
help_text=_( 
'Economic growth factor (>0.0). 1.0=no growth. 1.1 = 10%% growth etc.'))
Do's and don't's 
Do 
● Create JSON dumps and your own 
scripts for re-importing into 
your factored out application 
● Backup stuff 
● Make proxy models! 
● Be smart so you have to 
refactor as little code as 
possible. 
● Take small steps 
● Add new fields 
● Use regex for search and 
replace! 
Don't 
● Search and replace more than 
you can remember at once 
● Remove models 
● Rename models 
● Rename fields 
● End up with ambiguous stuff. 
Having to models is fine, 
having the same field in two 
places is not.
Put a proxy in place of the old model 
class MyModelWhichWasMoved(models.Model): 
"""This model was moved while refactoring...""" 
class Meta: 
model = my_new_application.Model 
proxy = True
Cheat :) 
class BrokerIntialContracts(models.Model): 
def __init__(self, *args, **kwargs): 
models.Model.__init__(self, *args, **kwargs) 
# When using the 'toughroad' application, swap this model 
# for the toughroad proxy 
from toughroad_meta.settings import SWAP_TOUGHROAD_PROXIES 
if SWAP_TOUGHROAD_PROXIES: 
from toughroad.models import BrokerIntialContracts 
self.__class__ = BrokerIntialContracts
Another big “do”: 
Refactor while you're hot!
Refactoring to be multi-lingual
django-parler
django-parler provides Django model translations without 
nasty hacks. 
Features: 
● Nice admin integration. 
● Access translated attributes like regular attributes. 
● Automatic fallback to the default language. 
● Separate table for translated fields, compatible with 
django-hvad. 
● Plays nice with others, compatible with django-polymorphic, 
django-mptt and such: 
● No ORM query hacks. 
● Easy to combine with custom Manager or QuerySet 
classes. 
● Easy to construct the translations model manually when 
needed.
django-parler 
from django.db import models 
from parler.models import TranslatableModel, TranslatedFields 
class MyModel(TranslatableModel): 
translations = TranslatedFields( 
title = models.CharField(_("Title"), max_length=200) 
) 
def __unicode__(self): 
return self.title
django-parler (2) 
>>> object = MyModel.objects.all()[0] 
>>> object.get_current_language() 
'en' 
>>> object.title 
u'cheese omelet' 
>>> object.set_current_language('fr') # Only switches 
>>> object.title = "omelette du fromage" # Translation is created on demand. 
>>> object.save()
So that means: 
mymodel.my_field willl still work!
But there is a fundamental issue! 
MyModel.my_field is no longer 
in the “myapp_my_model” table! 
It is in “myapp_my_model_translations”
So back to refactoring...
1 
Create the translation table, keep the 
existing columns 
2 
Copy the data from the original table to the 
translation table. 
3 
Remove the fields from the original model. 
See: 
http://django-parler.readthedocs.org/en/latest/advanced/migrating.html
4 
Refactor like hell
1: Add translations to model 
# Old model 
class MyModel(models.Model): 
name = models.CharField(max_length=123) 
# New model 
class MyModel(TranslatableModel): 
name = models.CharField(max_length=123) 
translations = TranslatedFields( 
name=models.CharField(max_length=123), 
)
2.1: Migrate the data 
# Create an empty data migration 
manage.py makemigrations --empty myapp "migrate_translatable_fields"
2.2: Create the migration 
def forwards_func(apps, schema_editor): 
MyModel = apps.get_model('myapp', 'MyModel') 
MyModelTranslation = apps.get_model('myapp', 'MyModelTranslation') 
for object in MyModel.objects.all(): 
MyModelTranslation.objects.create( 
master_id=object.pk, 
language_code=settings.LANGUAGE_CODE, 
name=object.name 
)
3 
Remove the old fields 
manage.py schemamigration myapp --auto 
"remove_untranslated_fields"
Refactoring necessary: 
This is broken... 
my_model.objects.filter(old_field=xx) 
But how often do you filter on translated 
strings?
Replace 
filter(field_name) 
with 
.translated(field_name) 
or 
filter(translations__field_name).
ModelAdmin 
from parler.admin import TranslatableAdmin 
class MyModelAdmin(TranslatableAdmin): 
search_fields = ('translations__title',)
Update the ordering and order_by() code 
TIP: Try to avoid default ordering by 
translated fields!
ModelAdmin 
from parler.admin import TranslatableAdmin 
class MyModelAdmin(TranslatableAdmin): 
search_fields = ('translations__title',)
Nasty stuff: Aggregation
lang = translation.get_language() 
contracts_owned = contracts_owned.filter(supplier__translations__language_code=lang) 
contracts_owned = contracts_owned.filter(country__translations__language_code=lang) 
contracts_owned = contracts_owned.filter(commodity__translations__language_code=lang) 
contracts_owned = contracts_owned.values('commodity__translations__name', 
'country__translations__name', 
'country__country_code', 
'supplier__translations__name', 
'commodity__tons', 
'latest_exchange_rate', 
'delivery_round__number', 
'supplier', 'commodity', 'country') 
contracts_owned = contracts_owned.annotate( 
tons=Sum('commodity__tons'), 
bags=Count('supplier__translations__name'), 
price=Sum('latest_exchange_rate') 
).order_by('delivery_round__number')
Refactoring: 
Just do it! 
...but in small steps.
Search and replace: 
Always search more than you replace 
Manually replace stuff!
May the schwartz be with you 
Github: benjaoming

More Related Content

PPTX
Mongo db mug_2012-02-07
PPTX
JavaScript Objects and OOP Programming with JavaScript
PDF
Map/reduce, geospatial indexing, and other cool features (Kristina Chodorow)
PDF
Desarrollo de módulos en Drupal e integración con dispositivos móviles
PDF
MongoDB With Style
PPTX
Game dev 101 part 2
PDF
Optimization in django orm
PDF
Modern Application Foundations: Underscore and Twitter Bootstrap
Mongo db mug_2012-02-07
JavaScript Objects and OOP Programming with JavaScript
Map/reduce, geospatial indexing, and other cool features (Kristina Chodorow)
Desarrollo de módulos en Drupal e integración con dispositivos móviles
MongoDB With Style
Game dev 101 part 2
Optimization in django orm
Modern Application Foundations: Underscore and Twitter Bootstrap

What's hot (20)

PDF
The Ring programming language version 1.9 book - Part 62 of 210
PPT
jQuery Loves You
PDF
Prototype UI
PDF
From mysql to MongoDB(MongoDB2011北京交流会)
PDF
The Ring programming language version 1.3 book - Part 42 of 88
PDF
Node meetup feb_20_12
PDF
Drupal & javascript
PDF
jQuery UI Widgets, Drag and Drop, Drupal 7 Javascript
PDF
Prototype UI Intro
KEY
Mapping Flatland: Using MongoDB for an MMO Crossword Game (GDC Online 2011)
PDF
2013-03-23 - NoSQL Spartakiade
PDF
Jquery In Rails
PPTX
A miało być tak... bez wycieków
PPTX
Django - sql alchemy - jquery
PPTX
Cnam azure 2014 mobile services
PDF
HTML5 Canvas - The Future of Graphics on the Web
KEY
Thoughts on MongoDB Analytics
PDF
Is HTML5 Ready? (workshop)
PDF
Cleaner, Leaner, Meaner: Refactoring your jQuery
PPTX
Scrollytelling
The Ring programming language version 1.9 book - Part 62 of 210
jQuery Loves You
Prototype UI
From mysql to MongoDB(MongoDB2011北京交流会)
The Ring programming language version 1.3 book - Part 42 of 88
Node meetup feb_20_12
Drupal & javascript
jQuery UI Widgets, Drag and Drop, Drupal 7 Javascript
Prototype UI Intro
Mapping Flatland: Using MongoDB for an MMO Crossword Game (GDC Online 2011)
2013-03-23 - NoSQL Spartakiade
Jquery In Rails
A miało być tak... bez wycieków
Django - sql alchemy - jquery
Cnam azure 2014 mobile services
HTML5 Canvas - The Future of Graphics on the Web
Thoughts on MongoDB Analytics
Is HTML5 Ready? (workshop)
Cleaner, Leaner, Meaner: Refactoring your jQuery
Scrollytelling
Ad

Similar to Strategies for refactoring and migrating a big old project to be multilingual and use multiple databases or how I learned to stop worrying and search and replace my code base. (20)

PDF
Data herding
PDF
Data herding
PDF
The Best (and Worst) of Django
PDF
Django Good Practices
PDF
PDF
Utopia Kingdoms scaling case. From 4 users to 50.000+
PDF
Utopia Kindgoms scaling case: From 4 to 50K users
PPTX
Web development with django - Basics Presentation
PDF
Django Overview
KEY
Introduction to Django
PPTX
SaaSy maps - using django-tenants and geodjango to provide web-gis software-a...
PDF
Django - basics
PDF
Django tips & tricks
PPTX
Basic Python Django
PDF
Introduction to django
PDF
django_reference_sheet
PDF
Building a custom cms with django
PDF
Django Workflow and Architecture
KEY
What's new in Django 1.2?
PPTX
Clustrix Database Percona Ruby on Rails benchmark
Data herding
Data herding
The Best (and Worst) of Django
Django Good Practices
Utopia Kingdoms scaling case. From 4 users to 50.000+
Utopia Kindgoms scaling case: From 4 to 50K users
Web development with django - Basics Presentation
Django Overview
Introduction to Django
SaaSy maps - using django-tenants and geodjango to provide web-gis software-a...
Django - basics
Django tips & tricks
Basic Python Django
Introduction to django
django_reference_sheet
Building a custom cms with django
Django Workflow and Architecture
What's new in Django 1.2?
Clustrix Database Percona Ruby on Rails benchmark
Ad

Recently uploaded (20)

PDF
MIND Revenue Release Quarter 2 2025 Press Release
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
Unlocking AI with Model Context Protocol (MCP)
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PPT
Teaching material agriculture food technology
PDF
Chapter 3 Spatial Domain Image Processing.pdf
PDF
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
PDF
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PDF
Mobile App Security Testing_ A Comprehensive Guide.pdf
PPTX
Big Data Technologies - Introduction.pptx
PPTX
MYSQL Presentation for SQL database connectivity
PPTX
Digital-Transformation-Roadmap-for-Companies.pptx
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PDF
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
PPTX
Cloud computing and distributed systems.
PPTX
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
PPTX
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
MIND Revenue Release Quarter 2 2025 Press Release
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
The Rise and Fall of 3GPP – Time for a Sabbatical?
Unlocking AI with Model Context Protocol (MCP)
Agricultural_Statistics_at_a_Glance_2022_0.pdf
Teaching material agriculture food technology
Chapter 3 Spatial Domain Image Processing.pdf
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
Mobile App Security Testing_ A Comprehensive Guide.pdf
Big Data Technologies - Introduction.pptx
MYSQL Presentation for SQL database connectivity
Digital-Transformation-Roadmap-for-Companies.pptx
Reach Out and Touch Someone: Haptics and Empathic Computing
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
Cloud computing and distributed systems.
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...

Strategies for refactoring and migrating a big old project to be multilingual and use multiple databases or how I learned to stop worrying and search and replace my code base.

  • 1. Strategies for refactoring and migrating a big old project to be multilingual and use multiple databases or how I learned to stop worrying and search and replace my code base. 8th Django Copenhagen Meetup Benjamin Bach benjamin@overtag.dk
  • 2. Thanks! Title backup responsible: @valberg
  • 3. 1 The project “toughroad” 2 multiple database schemas 3 multilingual (django-parler)
  • 5. A game: 80-250 players Educational (Global trade issues) ~6 hours Physical role play + computer interactions
  • 6. Illustration of a global trade chain: Farmers Traders Exporters Banks Brokers Brand companies Cafés  
  • 10. Testability Unfeasible: Either get 80 people or simulate 80 people's interactions Even worse: Every role is unique and there are up to 250+ Instruction manuals, interdependent human behavior, human errors are part of the game.
  • 11. 2009: First games played. 2010: First successful game. Fixing issues during gameplay for ~2 years Summer 2014: Game has worked flawlessly for a couple of years.
  • 12. http://cloc.sourceforge.net v 1.60 T=2.09 s (89.9 files/s, 13062.9 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Python 81 2948 1062 12832 HTML 100 1519 2 7326 CSS 3 133 31 953 Javascript 3 43 2 443 Bourne Shell 1 4 1 8 ------------------------------------------------------------------------------- SUM: 188 4647 1098 21562 ------------------------------------------------------------------------------- (september 2014)
  • 13. Finally! Success! New partners, more attention, new problems. Does it translate to other countries? Does it scale?
  • 14. 1. Copying the game Old model: For every game, a new database. Each game shares copies “start-up” configuration from a prototype game. (MySQL)
  • 15. New model: Use Postgres schemas! Now we can deploy each game inside its own schema and access shared data from the “public” schema.
  • 16. Schemas Namespaces inside a database Like adding set(A1,B1) + set(A2,C2) = set(A1,B1,C2)
  • 17. Example: Database:Google auth_userpublic + docsdocs + spreadsheetspreadsheet
  • 18. Scalability and performance win! Manage large sets of data separately Share tables only where necessary Reduce use of managers
  • 19. Downside: Hard to share data Makes migrations harder!
  • 20. Using schemas is a fundamental Design decision!
  • 21. Step 1: Setting up the project... 1. settings.DATABASES 2. settings.DATABASE_ROUTERS
  • 22. DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'toughroad_dk', 'USER': 'django', 'PASSWORD': 'django', 'HOST': '127.0.0.1', 'OPTIONS': { 'options': '-c search_path=public,' + os.environ['TOUGHROAD_SCHEMA'] }, }, 'toughroad': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'toughroad_dk', 'USER': 'django', 'PASSWORD': 'django', 'HOST': '127.0.0.1', 'OPTIONS': { 'options': '-c search_path=' + os.environ['TOUGHROAD_SCHEMA'] + ',public' }, }, } settings.DATABASES
  • 23. settings.DATABASE_ROUTERS DATABASE_ROUTERS = ['toughroad.database_routers.ToughroadRouter']
  • 24. APPS = ('toughroad', 'sessions') class ToughroadRouter(object): """ Put all game-specific data in a seperate database """ def db_for_read(self, model, **hints): if model._meta.app_label in APPS: return 'toughroad' return None def db_for_write(self, model, **hints): if model._meta.app_label in APPS: return 'toughroad' return None def allow_syncdb(self, db, model): if db == 'toughroad': return model._meta.app_label in APPS elif model._meta.app_label in APPS: return False return None def allow_relation(self, obj1, obj2, **hints): """ Allow relations if a model in the auth app is involved. """ if obj1._meta.app_label == 'default' or obj2._meta.app_label == 'toughroad': return True return None
  • 25. # Specifying the DB from command line: $ python manage.py shell –database=toughroad # Using env stuff $ TOUGHROAD_SCHEMA='customer_A' python manage.py shell –database=toughroad
  • 26. But why have different databases? Storing session data (south_)migration_history!
  • 27. Step 2: Deployment. No fun using schemas if they're not automatically handled
  • 28. manage.py deploy_game for game in games.filter(db_created=False): print "Creating schema for", game cursor = connection.cursor() cursor.execute("CREATE SCHEMA "" + game.schema_name + """) transaction.commit() ...
  • 29. manage.py deploy_game (2) for game in games.filter(db_created=False): ... # syncdb for applications that do not have migrations and # are not in the public schema already env = {} env.update(os.environ.copy()) env.update(game.env) p = subprocess.Popen( ['python', 'manage.py', 'syncdb', '--settings=settings.from_env', '--database=toughroad', '--noinput', '-- traceback'], env=env ) p.communicate() if p.returncode != 0: raise RuntimeError("Tried command, it failed") ...
  • 30. manage.py deploy_game (3) for game in games.filter(db_created=False): ... if trmeta_settings.BEFORE_MIGRATE: p = subprocess.Popen( shlex.split(trmeta_settings.BEFORE_MIGRATE), env=env ) p.communicate() if p.returncode != 0: raise RuntimeError("Tried command, it failed") ...
  • 31. manage.py deploy_game (4) for game in games.filter(db_created=False): ... if trmeta_settings.BEFORE_MIGRATE: p = subprocess.Popen( shlex.split(trmeta_settings.BEFORE_MIGRATE), env=env ) p.communicate() if p.returncode != 0: raise RuntimeError("Tried command, it failed") ...
  • 32. manage.py deploy_game (5) for game in games.filter(db_created=False): ... p = subprocess.Popen( ['python', 'manage.py', 'deploy_game', 'south', '--settings=settings.from_env', '--traceback'], env=env, ) p.communicate() if p.returncode != 0: raise RuntimeError("Tried command, it failed") game.db_created = True game.save() transaction.commit() ...
  • 33. manage.py deploy_game south ... from django.db import connections, connection if options['south']: cursor = connections['toughroad_explicit'].cursor() cursor.execute("""CREATE TABLE "{:s}"."south_migrationhistory" ( "id" serial NOT NULL PRIMARY KEY, "app_name" varchar(255) NOT NULL, "migration" varchar(255) NOT NULL, "applied" timestamp with time zone NOT NULL ) """.format(os.environ['TOUGHROAD_SCHEMA'])) transaction.commit_unless_managed(using="toughroad_explicit") cursor.close()
  • 34. manage.py deploy_game (6) for game in games.filter(db_created=False): ... print "Now migrating", game.schema_name env = {} env.update(os.environ.copy()) env.update(game.env) p = subprocess.Popen( ['python', 'manage.py', 'migrate', 'toughroad', '--settings=settings.from_env', '--database=toughroad', '--no-initial-data', '--noinput', '--traceback'], env=env ) p.communicate() if p.returncode != 0: raise RuntimeError("Tried command, it failed") ...
  • 35. manage.py deploy_game (7) for game in games.filter(db_created=False): ... if trmeta_settings.AFTER_MIGRATE: p = subprocess.Popen( shlex.split(trmeta_settings.AFTER_MIGRATE), env=env ) p.communicate() if p.returncode != 0: raise RuntimeError("Tried command, it failed") ...
  • 37. Splitting into separate applications! toughroad toughroad_meta
  • 38. “toughroad” before class GameRound(models.Model): """ A simulated year. Managed by "gameloop", all other threads are notified of round changes. """ number = models.IntegerField( _('round number'), unique=True, help_text=_('Counts from 1!')) template_duration = models.IntegerField( _('duration'), default=30, ) economic_growth = models.FloatField( default=1.0, verbose_name=_('economic growth'), help_text=_( 'Economic growth factor (>0.0). 1.0=no growth. 1.1 = 10%% growth etc.')) is_started = models.BooleanField(default=False) started_on = models.DateTimeField(null=True, blank=True)
  • 39. “toughroad” after from toughroad_meta.models import GameRound as GameRoundMeta class GameRound(GameRoundMeta): is_started = models.BooleanField(default=False) started_on = models.DateTimeField(null=True, blank=True)
  • 40. “toughroad_meta” class GameRound(models.Model): """ A simulated year. Managed by "gameloop", all other threads are notified of round changes. """ number = models.IntegerField( _('round number'), unique=True, help_text=_('Counts from 1!')) template_duration = models.IntegerField( _('duration'), default=30, ) economic_growth = models.FloatField( default=1.0, verbose_name=_('economic growth'), help_text=_( 'Economic growth factor (>0.0). 1.0=no growth. 1.1 = 10%% growth etc.'))
  • 41. Do's and don't's Do ● Create JSON dumps and your own scripts for re-importing into your factored out application ● Backup stuff ● Make proxy models! ● Be smart so you have to refactor as little code as possible. ● Take small steps ● Add new fields ● Use regex for search and replace! Don't ● Search and replace more than you can remember at once ● Remove models ● Rename models ● Rename fields ● End up with ambiguous stuff. Having to models is fine, having the same field in two places is not.
  • 42. Put a proxy in place of the old model class MyModelWhichWasMoved(models.Model): """This model was moved while refactoring...""" class Meta: model = my_new_application.Model proxy = True
  • 43. Cheat :) class BrokerIntialContracts(models.Model): def __init__(self, *args, **kwargs): models.Model.__init__(self, *args, **kwargs) # When using the 'toughroad' application, swap this model # for the toughroad proxy from toughroad_meta.settings import SWAP_TOUGHROAD_PROXIES if SWAP_TOUGHROAD_PROXIES: from toughroad.models import BrokerIntialContracts self.__class__ = BrokerIntialContracts
  • 44. Another big “do”: Refactor while you're hot!
  • 45. Refactoring to be multi-lingual
  • 47. django-parler provides Django model translations without nasty hacks. Features: ● Nice admin integration. ● Access translated attributes like regular attributes. ● Automatic fallback to the default language. ● Separate table for translated fields, compatible with django-hvad. ● Plays nice with others, compatible with django-polymorphic, django-mptt and such: ● No ORM query hacks. ● Easy to combine with custom Manager or QuerySet classes. ● Easy to construct the translations model manually when needed.
  • 48. django-parler from django.db import models from parler.models import TranslatableModel, TranslatedFields class MyModel(TranslatableModel): translations = TranslatedFields( title = models.CharField(_("Title"), max_length=200) ) def __unicode__(self): return self.title
  • 49. django-parler (2) >>> object = MyModel.objects.all()[0] >>> object.get_current_language() 'en' >>> object.title u'cheese omelet' >>> object.set_current_language('fr') # Only switches >>> object.title = "omelette du fromage" # Translation is created on demand. >>> object.save()
  • 50. So that means: mymodel.my_field willl still work!
  • 51. But there is a fundamental issue! MyModel.my_field is no longer in the “myapp_my_model” table! It is in “myapp_my_model_translations”
  • 52. So back to refactoring...
  • 53. 1 Create the translation table, keep the existing columns 2 Copy the data from the original table to the translation table. 3 Remove the fields from the original model. See: http://django-parler.readthedocs.org/en/latest/advanced/migrating.html
  • 55. 1: Add translations to model # Old model class MyModel(models.Model): name = models.CharField(max_length=123) # New model class MyModel(TranslatableModel): name = models.CharField(max_length=123) translations = TranslatedFields( name=models.CharField(max_length=123), )
  • 56. 2.1: Migrate the data # Create an empty data migration manage.py makemigrations --empty myapp "migrate_translatable_fields"
  • 57. 2.2: Create the migration def forwards_func(apps, schema_editor): MyModel = apps.get_model('myapp', 'MyModel') MyModelTranslation = apps.get_model('myapp', 'MyModelTranslation') for object in MyModel.objects.all(): MyModelTranslation.objects.create( master_id=object.pk, language_code=settings.LANGUAGE_CODE, name=object.name )
  • 58. 3 Remove the old fields manage.py schemamigration myapp --auto "remove_untranslated_fields"
  • 59. Refactoring necessary: This is broken... my_model.objects.filter(old_field=xx) But how often do you filter on translated strings?
  • 60. Replace filter(field_name) with .translated(field_name) or filter(translations__field_name).
  • 61. ModelAdmin from parler.admin import TranslatableAdmin class MyModelAdmin(TranslatableAdmin): search_fields = ('translations__title',)
  • 62. Update the ordering and order_by() code TIP: Try to avoid default ordering by translated fields!
  • 63. ModelAdmin from parler.admin import TranslatableAdmin class MyModelAdmin(TranslatableAdmin): search_fields = ('translations__title',)
  • 65. lang = translation.get_language() contracts_owned = contracts_owned.filter(supplier__translations__language_code=lang) contracts_owned = contracts_owned.filter(country__translations__language_code=lang) contracts_owned = contracts_owned.filter(commodity__translations__language_code=lang) contracts_owned = contracts_owned.values('commodity__translations__name', 'country__translations__name', 'country__country_code', 'supplier__translations__name', 'commodity__tons', 'latest_exchange_rate', 'delivery_round__number', 'supplier', 'commodity', 'country') contracts_owned = contracts_owned.annotate( tons=Sum('commodity__tons'), bags=Count('supplier__translations__name'), price=Sum('latest_exchange_rate') ).order_by('delivery_round__number')
  • 66. Refactoring: Just do it! ...but in small steps.
  • 67. Search and replace: Always search more than you replace Manually replace stuff!
  • 68. May the schwartz be with you Github: benjaoming