Implement get_view_str approach.

A couple of other changes:

* AppRegistryNotReady fix (via both imports changes and docs update).
* Non-flat models structure support (app.models.submodels.Model).
* Minor refactoring.
master
murchik 2020-11-19 13:42:29 +08:00
parent 4cb8a7a28a
commit 069e0a66b2
3 changed files with 70 additions and 47 deletions

View File

@ -6,23 +6,12 @@ So far only MySQL is supported as backend, but more could be added if necessary.
## Quick start ## Quick start
1. Add "dbview" to your INSTALLED_APPS settings like this: 1. In your models.py create classes which extend dbview.DbView like this:
```python
INSTALLED_APPS = (
...
'dbview',
)
```
2. In your models.py create classes which extend dbview.DbView
like this:
```python ```python
from django.db import models from django.db import models
from dbview import DbView from dbview.models import DbView
class ModelA(models.Model): class ModelA(models.Model):
fielda = models.CharField() fielda = models.CharField()
@ -34,7 +23,7 @@ like this:
fieldB = models.IntegerField(blank=True, null=True, db_column='fieldb') fieldB = models.IntegerField(blank=True, null=True, db_column='fieldb')
@classmethod @classmethod
def view(klass): def view(cls):
''' '''
This method returns the SQL string that creates the view, in this This method returns the SQL string that creates the view, in this
example fieldB is the result of annotating another column example fieldB is the result of annotating another column
@ -47,10 +36,25 @@ like this:
return str(qs.query) return str(qs.query)
``` ```
Alternatively `get_view_str` method could be used to write a custom SQL:
3. Then create a migration point for your view generation, edit that migration ```python
and modify it, add: `from dbview import CreateView` and replace the line
the call to `migrations.CreateModel` with `CreateView`.
class MyView(DbView):
4. Migrate your database and start using your database views. # ...
@classmethod
def get_view_str(cls):
return """
CREATE VIEW my_view AS (
SELECT ...
)
"""
```
2. Then create a migration point for your view generation, edit that migration
and modify it, add: `from dbview.helpers import CreateView` and replace the
line the call to `migrations.CreateModel` with `CreateView`.
3. Migrate your database and start using your database views.

View File

@ -1,3 +0,0 @@
# placeholder for git
from dbview.helpers import CreateView
from dbview.models import DbView

View File

@ -1,42 +1,64 @@
import logging
from django.db import migrations from django.db import migrations
from django.apps import apps from django.apps import apps
class CreateView(migrations.CreateModel): class CreateView(migrations.CreateModel):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
model = to_state.apps.get_model(app_label, self.name) fake_model = to_state.apps.get_model(app_label, self.name)
if not self.allow_migrate_model(schema_editor.connection.alias, model): if not self.allow_migrate_model(
schema_editor.connection.alias, fake_model):
raise raise
model = self._get_model(fake_model, app_label, to_state)
self._drop_view(fake_model, schema_editor)
if hasattr(model, 'view'):
self._create_standard_view(model, schema_editor)
elif hasattr(model, 'get_view_str'):
self._create_view_from_raw_sql(model.get_view_str(), schema_editor)
else:
raise Exception(f"{model} has neither view nor get_view_str")
def database_backwards(self, app_label, schema_editor, from_state, to):
fake_model = from_state.apps.get_model(app_label, self.name)
self._drop_view(fake_model, schema_editor)
def _get_model(self, state, app_label, fake_model):
models = apps.get_app_config(app_label).models_module models = apps.get_app_config(app_label).models_module
model = getattr(models, self.name)
sql = 'DROP VIEW IF EXISTS %(table)s;' if hasattr(models, self.name):
return getattr(models, self.name)
# TODO: recursive search
for submodule in models.__dict__.values():
if hasattr(submodule, self.name):
return getattr(submodule, self.name)
logging.warning('Using fake model, this may fail with inherited views')
return fake_model
def _drop_view(self, model, schema_editor):
sql_template = 'DROP VIEW IF EXISTS %(table)s CASCADE'
args = { args = {
'table': schema_editor.quote_name(model._meta.db_table), 'table': schema_editor.quote_name(model._meta.db_table),
} }
sql = sql_template % args
sql = sql % args
schema_editor.execute(sql, None) schema_editor.execute(sql, None)
sql = 'CREATE VIEW %(table)s AS %(definition)s' def _create_standard_view(self, model, schema_editor):
sql_template = 'CREATE VIEW %(table)s AS %(definition)s'
if hasattr(model, 'view'):
qs = str(model.view()) qs = str(model.view())
else: args = {
raise Exception('Your view needs to define either view or ' + 'table': schema_editor.quote_name(model._meta.db_table),
'get_view_str') 'definition': qs,
}
args['definition'] = qs sql = sql_template % args
self._create_view_from_raw_sql(sql, schema_editor)
sql = sql % args
schema_editor.execute(sql, None) def _create_view_from_raw_sql(self, sql, schema_editor):
def database_backwards(self, app_label, schema_editor, from_state, to):
model = from_state.apps.get_model(app_label, self.name)
sql = 'DROP VIEW IF EXISTS %s' % \
schema_editor.quote_name(model._meta.db_table)
schema_editor.execute(sql, None) schema_editor.execute(sql, None)