#23749 closed Cleanup/optimization (fixed)
Add an example of using database routers in migrations
| Reported by: | Alfred Perlstein | Owned by: | |
|---|---|---|---|
| Component: | Documentation | Version: | 1.7 |
| Severity: | Normal | Keywords: | migrations dbrouter |
| Cc: | me@… | Triage Stage: | Ready for checkin |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | yes | UI/UX: | no |
Description
It seems that the migration system in 1.7 does not properly query the dbrouter setup when a migrations.RunPython method is invoked.
Running the following command:
python manage.py migrate mycompany --noinput --traceback --database=logs --verbosity=2
Never seems to call the router for the following migration, so as you can see I've added code to parse sys.argv as an experiment which seems to sort-of work, but really looks like the wrong way to do this.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from applianceUI.mycompany.dbrouter import LogRouter
import sys
def bkd_default_ports(apps, schema_editor):
db = None
for arg in sys.argv:
if '=' not in arg:
continue
print "arg: ", arg
k, v = arg.split('=', 2)
print "k: '%s', v: '%s' " % (k,v)
if k == "--database":
print arg
db = v
break
print "db is %s" % db
router = LogRouter()
Blockd = apps.get_model("mycompany", "Blockd")
if router.allow_migrate(db, Blockd) is False:
return
try:
bkd = Blockd.objects.order_by("-id")[0]
except IndexError:
bkd = Blockd.objects.create()
if bkd.bkd_ports is "":
bkd.bkd_ports = "80, 8080, 3128"
bkd.save()
class Migration(migrations.Migration):
dependencies = [
('mycompany', '0005_auto_direction_and_indexes'),
]
operations = [
migrations.RunPython(bkd_default_ports),
]
The arguments apps and schema_editor didn't give me enough data to make my decision. How can i properly derive that the database is "logs"?
Attachments (1)
Change History (28)
comment:1 by , 11 years ago
comment:2 by , 11 years ago
I think this is a duplicate of #23273 (or at least closely related to). Have you read that ticket?
comment:3 by , 11 years ago
To be honest this is more like #22583.
#23273 is too complex for me to parse, there may be some overlap, but I do not think so.
All that is needed is that we somehow pass the --database=FOO option to the RunPython method as a kwarg so that the router can provide a function to query that... OR that the migration itself can decide.
I am not sure why the name of the db is not given to the RunPython method.
This is a relatively simple request, can this please be fixed?
comment:4 by , 11 years ago
Ah yes, that was the ticket I was looking for but couldn't find - forgot it had been closed. Anyway, wouldn't schema_editor.connection.alias give you the name of the database? If so, we should likely document the solution.
comment:5 by , 11 years ago
| Component: | Migrations → Documentation |
|---|---|
| Easy pickings: | set |
| Summary: | migrations.RunPython broken with dbrouters. → Document how to get the database alias in migrations |
| Triage Stage: | Unreviewed → Accepted |
| Type: | Bug → Cleanup/optimization |
comment:6 by , 11 years ago
I just verified that schema_editor.connection.alias does return the current connection in use, even when --database=foobar is used. We could mention this in the data migration doc and/or the `SchemaEditor` doc. SchemaEditor doc could use a Properties section along with the Methods section.
Edit: Looks like it's already documented in the RunPython section https://github.com/django/django/blob/aa5ef0d4fc67a95ac2a5103810d0c87d8c547bac/docs/ref/migration-operations.txt#L316-L322
comment:7 by , 11 years ago
| Cc: | added |
|---|
comment:8 by , 11 years ago
| Resolution: | → worksforme |
|---|---|
| Status: | new → closed |
comment:9 by , 11 years ago
Can someone give me a pointer to how to edit the page
https://docs.djangoproject.com/en/1.7/topics/migrations/#data-migrations
-Or-
https://docs.djangoproject.com/en/1.7/ref/schema-editor/
?
To be perfectly honest the addition to the RunPython section (https://github.com/django/django/blob/aa5ef0d4fc67a95ac2a5103810d0c87d8c547bac/docs/ref/migration-operations.txt#L316-L322) is not something that I would have been able to find via google, nor is it likely I would have been able to understand it.
I'm perfectly willing to update the docs for the migration or schema_editor pages, I just need a pointer how to get started. Is it a wiki I can log into? Or is there a git repo I can clone?
Please advise.
thank you!
-Alfred
comment:10 by , 11 years ago
| Resolution: | worksforme |
|---|---|
| Status: | closed → new |
Please see Writing documentation.
comment:11 by , 11 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
I have created a pull request against documentation here:
comment:12 by , 11 years ago
If this looks OK I would like to document the connection handle being inside the schema_editor on another page. Please let me know if this document addition is in the correct path and if so I will look into augmenting the schema_editor page.
Is it the norm to document member variables? Is there an existing example? The reason I ask is that schema_editor only has its methods documented and no member variables.
thank you.
comment:13 by , 11 years ago
Tim,
I've submitted a github pull request and tried my best to follow the guide on submitting patches.
I have no spelling errors and no warnings that are in files I have touched.
Can the github pull request be merged at your convenience?
comment:14 by , 11 years ago
| Owner: | removed |
|---|---|
| Status: | assigned → new |
comment:15 by , 11 years ago
| Has patch: | set |
|---|
comment:16 by , 11 years ago
| Patch needs improvement: | set |
|---|
follow-up: 18 comment:17 by , 11 years ago
#23879 is somewhat related. See this POC branch: https://github.com/wrwrwr/django/compare/feature/always-test-gis-and-postgres
follow-up: 19 comment:18 by , 11 years ago
Replying to claudep:
#23879 is somewhat related. See this POC branch: https://github.com/wrwrwr/django/compare/feature/always-test-gis-and-postgres
TBH, I don't see any relation to the issue here. It's not about running on a specific vendor, it's about different databases, probably even of the same vendor.
comment:19 by , 11 years ago
Replying to MarkusH:
Replying to claudep:
#23879 is somewhat related. See this POC branch: https://github.com/wrwrwr/django/compare/feature/always-test-gis-and-postgres
TBH, I don't see any relation to the issue here. It's not about running on a specific vendor, it's about different databases, probably even of the same vendor.
+1
comment:20 by , 11 years ago
-
docs/ref/schema-editor.txt
diff --git a/docs/ref/schema-editor.txt b/docs/ref/schema-editor.txt index 54dd3bf..c503296 100644
a b below. 158 158 159 159 .. attribute:: SchemaEditor.connection 160 160 161 A connection object to the database. A useful attribute of the 162 connection is ``alias`` which can be used to determine the name of 163 the database beingaccessed.161 A connection object to the database. A useful attribute of the connection is 162 ``alias`` which can be used to determine the name of the database being 163 accessed. 164 164 165 This in turn is useful when doing data migrations for 166 :ref:`migrations withmultiple database backends <data-migrations-and-multiple-databases>`.165 This in turn is useful when doing data migrations for :ref:`migrations with 166 multiple database backends <data-migrations-and-multiple-databases>`. -
docs/topics/migrations.txt
diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index 746f4d2..a743bf2 100644
a b backwards will raise an exception. 472 472 Data migrations and multiple databases 473 473 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 474 474 475 One of the challenges that can be encountered is figuring out whether 476 or not to run a migration when using multiple databases. 477 478 For example, you may want to **only** run a migration against a 479 particular database. 480 481 In order to do that you can filter inside of your data migration 482 by looking at the ``schema_editor.connection.alias`` attribute. 483 However it is better to have your ``dbrouter`` leverage the same 484 ``allow_migrate`` call by making the following method in your 485 router:: 486 487 class MyRouter(object): 488 489 def allow_migrate(self, db, model): 490 # your typical migration code here 491 492 def runpython_ok(self, apps, schema_editor, model): 493 db = schema_editor.connection.alias 494 if self.allow_migrate(db, model): 495 return True 496 return False 497 498 Then to leverage this in your migrations, do the following:: 499 500 # -*- coding: utf-8 -*- 501 from __future__ import unicode_literals 502 503 from django.db import models, migrations 504 from myappname.dbrouter import MyRouter 505 import sys 506 507 def forwards(apps, schema_editor): 508 # only run this for the correct databases 509 MyModel = apps.get_model("myappname", "MyModel") 510 if not MyRouter().runpython_ok(apps, schema_editor, MyModel): 511 return 512 513 """ 514 This is a global configuration model so there should be only 515 row. 516 Get the first one -or- create the initial one. 517 Only set the default ports if it has not been set by user 518 already. 519 """ 520 try: 521 my_model = MyModel.objects.order_by("-id").first() 522 except IndexError: 523 my_model = MyModel.objects.create() 524 if my_model.my_model_ports == "": 525 my_model.my_model_ports = "80, 8080, 3128" 526 my_model.save() 527 528 def backwards(apps, schema_editor): 529 pass 530 531 class Migration(migrations.Migration): 532 533 dependencies = [ 534 # Dependencies to other migrations 535 ] 536 537 operations = [ 538 migrations.RunPython(forwards, backwards), 539 ] 475 One of the challenges that can be encountered is figuring out whether or not to 476 run a migration when using multiple databases. 477 478 For example, you may want to **only** run a migration against a particular 479 database. 480 481 In order to do that you can check the connection alias inside a ``RunPython`` 482 forwards and backwards operation by looking at the 483 ``schema_editor.connection.alias`` attribute. 484 485 # -*- coding: utf-8 -*- 486 from __future__ import unicode_literals 487 488 from django.db import migrations 489 490 def forwards(apps, schema_editor): 491 if not schema_editor.connection.alias == 'master-db': 492 return 493 # Your migration code goes here 494 495 class Migration(migrations.Migration): 496 497 dependencies = [ 498 # Dependencies to other migrations 499 ] 500 501 operations = [ 502 migrations.RunPython(forwards), 503 ] 504 505 You can also use your database router's ``allow_migrate`` method, but keep in 506 mind that the imported router needs to stay around as long as it is referenced 507 inside a migration: 508 509 .. snippet:: 510 :filename: myapp/dbrouters.py 511 512 class MyRouter(object): 513 514 def allow_migrate(self, db, model): 515 return db == 'default': 516 517 Then, to leverage this in your migrations, do the following:: 518 519 # -*- coding: utf-8 -*- 520 from __future__ import unicode_literals 521 522 from django.db import models, migrations 523 524 from myappname.dbrouters import MyRouter 525 526 def forwards(apps, schema_editor): 527 MyModel = apps.get_model("myappname", "MyModel") 528 if not MyRouter().allow_migrate(schema_editor.connection.alias, MyModel): 529 return 530 # Your migration code goes here 531 532 class Migration(migrations.Migration): 533 534 dependencies = [ 535 # Dependencies to other migrations 536 ] 537 538 operations = [ 539 migrations.RunPython(forwards), 540 ] 540 541 541 542 More advanced migrations 542 543 ~~~~~~~~~~~~~~~~~~~~~~~~
by , 11 years ago
comment:21 by , 11 years ago
Which version of the pull request was this made against? It seems to be done against an older version and will not apply cleanly.
comment:24 by , 11 years ago
| Patch needs improvement: | unset |
|---|---|
| Triage Stage: | Accepted → Ready for checkin |
comment:25 by , 11 years ago
| Summary: | Document how to get the database alias in migrations → Add an example of using database routers in migrations |
|---|
comment:26 by , 11 years ago
| Owner: | set to |
|---|---|
| Resolution: | → fixed |
| Status: | new → closed |
Traceback I get is as follows:
+ psql -U pgsql -d postgres -c 'create database historicallogs' CREATE DATABASE + python manage.py migrate mycompany --noinput --traceback --database=logs Operations to perform: Apply all migrations: mycompany Running migrations: Applying mycompany.0001_initial... OK Applying mycompany.0002_auto_20140904_1549... OK Applying mycompany.0003_auto_20141006_1134... OK Applying mycompany.0004_blockd_bkd_ports... OK Applying mycompany.0005_auto_direction_and_indexes... OK Applying mycompany.0006_auto_20141103_1539...Traceback (most recent call last): File "manage.py", line 42, in <module> execute_from_command_line(sys.argv) File "/usr/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 385, in execute_from_command_line utility.execute() File "/usr/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 377, in execute self.fetch_command(subcommand).run_from_argv(self.argv) File "/usr/local/lib/python2.7/site-packages/django/core/management/base.py", line 288, in run_from_argv self.execute(*args, **options.__dict__) File "/usr/local/lib/python2.7/site-packages/django/core/management/base.py", line 338, in execute output = self.handle(*args, **options) File "/usr/local/lib/python2.7/site-packages/django/core/management/commands/migrate.py", line 160, in handle executor.migrate(targets, plan, fake=options.get("fake", False)) File "/usr/local/lib/python2.7/site-packages/django/db/migrations/executor.py", line 63, in migrate self.apply_migration(migration, fake=fake) File "/usr/local/lib/python2.7/site-packages/django/db/migrations/executor.py", line 97, in apply_migration migration.apply(project_state, schema_editor) File "/usr/local/lib/python2.7/site-packages/django/db/migrations/migration.py", line 107, in apply operation.database_forwards(self.app_label, schema_editor, project_state, new_state) File "/usr/local/lib/python2.7/site-packages/django/db/migrations/operations/special.py", line 117, in database_forwards self.code(from_state.render(), schema_editor) File "/usr/local/www/applianceUI/applianceUI/mycompany/migrations/0006_auto_20141103_1539.py", line 8, in bkd_default_ports bkd = Blockd.objects.config() File "/usr/local/www/applianceUI/applianceUI/freeadmin/models/__init__.py", line 59, in config obj = self.order_by("-id")[0] File "/usr/local/lib/python2.7/site-packages/django/db/models/query.py", line 177, in __getitem__ return list(qs)[0] File "/usr/local/lib/python2.7/site-packages/django/db/models/query.py", line 141, in __iter__ self._fetch_all() File "/usr/local/lib/python2.7/site-packages/django/db/models/query.py", line 966, in _fetch_all self._result_cache = list(self.iterator()) File "/usr/local/lib/python2.7/site-packages/django/db/models/query.py", line 265, in iterator for row in compiler.results_iter(): File "/usr/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 700, in results_iter for rows in self.execute_sql(MULTI): File "/usr/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 786, in execute_sql cursor.execute(sql, params) File "/usr/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 65, in execute return self.cursor.execute(sql, params) File "/usr/local/lib/python2.7/site-packages/django/db/utils.py", line 94, in __exit__ six.reraise(dj_exc_type, dj_exc_value, traceback) File "/usr/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 65, in execute return self.cursor.execute(sql, params) File "/usr/local/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 485, in execute return Database.Cursor.execute(self, query, params) django.db.utils.OperationalError: no such table: mycompany_blockd + su - pgsql -c '/usr/local/bin/pg_ctl stop -w -D /data/pgsql/data' waiting for server to shut down.... done server stopped + exit 1My dbrouter looks like such:
# Router for DB logs postgres_models = ('log', 'logrollup', 'ipwhitelist', 'urlwhitelist') tmplog_models = ('blocklog',) class LogRouter(object): ''' A router to control all database operations for historical logs. ''' def db_for_read(self, model, **hints): ''' Attempts to read historical logs go to logs db. ''' if model._meta.model_name in postgres_models: return 'logs' if model._meta.model_name in tmplog_models: return 'tmplogs' return None def db_for_write(self, model, **hints): ''' Attempts to read historical logs go to logs db. ''' if model._meta.model_name in postgres_models: return 'logs' if model._meta.model_name in tmplog_models: return 'tmplogs' return None def allow_relation(self, obj1, obj2, **hints): # XXX: need cy to revisit this. # "apps" is not defined during initial migrations for some # reason and this throws a traceback. #if obj1._meta.app_label in apps or obj2._meta.app_label in apps: # return True return None def allow_migrate(self, db, model): ''' Make sure the historical logs only appear in the 'logs' database. ''' if db == 'logs': return model._meta.model_name in postgres_models elif model._meta.model_name in postgres_models: return False if db == 'tmplogs': return model._meta.model_name in tmplog_models elif model._meta.model_name in tmplog_models: return False # otherwise do not allow migrations for non-logs/tmplogs migrations. if db == 'logs' or db == 'tmplogs': return False return None