| 1 | #!/usr/bin/python |
|---|
| 2 | # -*- encoding: utf-8 -*- |
|---|
| 3 | |
|---|
| 4 | ## |
|---|
| 5 | ## dbpickle.py |
|---|
| 6 | ## Created on Thu Nov 30 00:26:52 2006 |
|---|
| 7 | ## by akaihola |
|---|
| 8 | ## |
|---|
| 9 | ## Copyright (C) 2006 Antti Kaihola |
|---|
| 10 | ## This program is free software; you can redistribute it and/or modify |
|---|
| 11 | ## it under the terms of the GNU General Public License as published by |
|---|
| 12 | ## the Free Software Foundation; either version 2 of the License, or |
|---|
| 13 | ## (at your option) any later version. |
|---|
| 14 | ## |
|---|
| 15 | ## This program is distributed in the hope that it will be useful, |
|---|
| 16 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 17 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 18 | ## GNU General Public License for more details. |
|---|
| 19 | ## |
|---|
| 20 | ## You should have received a copy of the GNU General Public License |
|---|
| 21 | ## along with this program; if not, write to the Free Software |
|---|
| 22 | ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|---|
| 23 | ## |
|---|
| 24 | |
|---|
| 25 | """ |
|---|
| 26 | dbpickle.py is a Django tool for saving all database objects into a |
|---|
| 27 | file or loading and storing them back into the database. It's useful |
|---|
| 28 | for migrating from one database engine to another. |
|---|
| 29 | |
|---|
| 30 | This version handles ForeignKeys which refer to the related model's |
|---|
| 31 | primary key. ManyToMany relations are now supported, but OneToOne |
|---|
| 32 | relations are not. |
|---|
| 33 | |
|---|
| 34 | You can also use you own plugin for pre-processing objects before |
|---|
| 35 | saving them into the database. This is useful e.g. when a model has |
|---|
| 36 | changed and you're importing objects from an old version of the |
|---|
| 37 | database. The plugin should be a Python program with a |
|---|
| 38 | ``pre_save_hook(obj)`` and/or ``pre_walk_hook(obj)`` function which |
|---|
| 39 | returns True if it modified the object, otherwise False. |
|---|
| 40 | |
|---|
| 41 | Be careful with objects automatically inserted in your app e.g. in save() |
|---|
| 42 | methods. Those could mess up ID numbering in databases which use sequence |
|---|
| 43 | tables. |
|---|
| 44 | |
|---|
| 45 | |
|---|
| 46 | Usage example |
|---|
| 47 | ============= |
|---|
| 48 | |
|---|
| 49 | Let's suppose your project is using PostgreSQL and the database has |
|---|
| 50 | been populated using the app. |
|---|
| 51 | |
|---|
| 52 | $ PYTHONPATH=/home/myproject DJANGO_SETTINGS_MODULE=settings \ |
|---|
| 53 | ./dbpickle.py --dump --file=myproject.pickle |
|---|
| 54 | |
|---|
| 55 | At this point you could change the settings so that SQLite is used |
|---|
| 56 | instead of PostgreSQL. |
|---|
| 57 | |
|---|
| 58 | $ PYTHONPATH=/home/myproject DJANGO_SETTINGS_MODULE=settings \ |
|---|
| 59 | /home/myproject/manage.py syncdb |
|---|
| 60 | |
|---|
| 61 | (answer 'no' when asked about creating a superuser) |
|---|
| 62 | |
|---|
| 63 | $ PYTHONPATH=/home/myproject DJANGO_SETTINGS_MODULE=settings \ |
|---|
| 64 | ./dbpickle.py --load --file=myproject.pickle |
|---|
| 65 | |
|---|
| 66 | This would effectively convert your database from PostgreSQL to SQLite |
|---|
| 67 | through Django. |
|---|
| 68 | """ |
|---|
| 69 | |
|---|
| 70 | import cPickle |
|---|
| 71 | import logging |
|---|
| 72 | from imp import load_source |
|---|
| 73 | import os |
|---|
| 74 | |
|---|
| 75 | try: |
|---|
| 76 | set # Only available in Python 2.4+ |
|---|
| 77 | except NameError: |
|---|
| 78 | from sets import Set as set # Python 2.3 fallback |
|---|
| 79 | |
|---|
| 80 | def dump(filepath): |
|---|
| 81 | """ |
|---|
| 82 | Pickle all Django objects from the database and save them into the |
|---|
| 83 | given file. |
|---|
| 84 | """ |
|---|
| 85 | objects = {} # model instances |
|---|
| 86 | m2m_lists = [] # ManyToMany relations between model instances |
|---|
| 87 | for model in models.get_models(): |
|---|
| 88 | meta = model._meta |
|---|
| 89 | app, model_name = meta.app_label, meta.module_name |
|---|
| 90 | logging.info('dumping %s.%s' % (app, model_name)) |
|---|
| 91 | |
|---|
| 92 | # get all many-to-many relation field names for this model |
|---|
| 93 | m2ms = [m2m.name for m2m in meta.many_to_many] |
|---|
| 94 | |
|---|
| 95 | try: |
|---|
| 96 | for obj in model.objects.all(): |
|---|
| 97 | logging.debug('dumping %s.%s %r' % (app, model_name, obj)) |
|---|
| 98 | pk = obj._get_pk_val() |
|---|
| 99 | objects[app, model_name, pk] = obj |
|---|
| 100 | |
|---|
| 101 | for m2m in m2ms: |
|---|
| 102 | # store many-to-many related objects for every |
|---|
| 103 | # many-to-many relation of this object |
|---|
| 104 | foreign_objs = getattr(obj, m2m).all() |
|---|
| 105 | logging.debug('dumping %s.%s.%s x %d' % (app, model_name, m2m, len(foreign_objs))) |
|---|
| 106 | m2m_lists.append((obj, m2m, tuple(foreign_objs))) |
|---|
| 107 | except (backend.Database.OperationalError, |
|---|
| 108 | backend.Database.ProgrammingError), e: |
|---|
| 109 | logging.warning('table for %r not found (%s)' % (model, e)) |
|---|
| 110 | transaction.rollback_unless_managed() |
|---|
| 111 | |
|---|
| 112 | # pickle all objects and many-to-many relations on disk |
|---|
| 113 | cPickle.dump((objects, m2m_lists), file(filepath, 'w')) |
|---|
| 114 | |
|---|
| 115 | |
|---|
| 116 | #@transaction.commit_on_success |
|---|
| 117 | def load(filepath, pluginpath=None): |
|---|
| 118 | """ |
|---|
| 119 | Unpickle Django objects from the given file and save them into the |
|---|
| 120 | database. |
|---|
| 121 | """ |
|---|
| 122 | |
|---|
| 123 | # load the plugin if specified on the command line |
|---|
| 124 | if pluginpath: |
|---|
| 125 | plugin = load_source('plugin', pluginpath) |
|---|
| 126 | else: |
|---|
| 127 | plugin = None |
|---|
| 128 | |
|---|
| 129 | # get the hook functions from the plugin |
|---|
| 130 | hooks = {} |
|---|
| 131 | for hookname in 'pre_save', 'pre_walk': |
|---|
| 132 | hooks[hookname] = getattr(plugin, '%s_hook' % hookname, lambda obj: False) |
|---|
| 133 | |
|---|
| 134 | # unpickle objects and many-to-many relations from disk |
|---|
| 135 | objects, m2m_lists = cPickle.load(file(filepath)) |
|---|
| 136 | |
|---|
| 137 | # Find distinct models of all unpickled objects and delete all |
|---|
| 138 | # objects before loading. Note that models which have not been |
|---|
| 139 | # dumped are not emptied. |
|---|
| 140 | models = set( [obj.__class__ for obj in objects.itervalues()] ) |
|---|
| 141 | for model in models: |
|---|
| 142 | try: |
|---|
| 143 | for obj in model._default_manager.all(): |
|---|
| 144 | obj.delete() |
|---|
| 145 | except (backend.Database.OperationalError, |
|---|
| 146 | backend.Database.ProgrammingError), e: |
|---|
| 147 | logging.warning('table for %r not found (%s)' % (model, e)) |
|---|
| 148 | transaction.rollback_unless_managed() |
|---|
| 149 | |
|---|
| 150 | # load all objects |
|---|
| 151 | while objects: |
|---|
| 152 | key, obj = objects.popitem() |
|---|
| 153 | load_recursive(objects, obj, hooks) |
|---|
| 154 | |
|---|
| 155 | # load all many-to-many relations |
|---|
| 156 | for obj1, m2m, foreign_objs in m2m_lists: |
|---|
| 157 | meta1 = obj1._meta |
|---|
| 158 | for obj2 in foreign_objs: |
|---|
| 159 | meta2 = obj2._meta |
|---|
| 160 | logging.debug('loading ManyToMany %s.%s.%s -> %s.%s.%s' % ( |
|---|
| 161 | meta1.app_label, meta1.module_name, obj1._get_pk_val(), |
|---|
| 162 | meta2.app_label, meta2.module_name, obj2._get_pk_val())) |
|---|
| 163 | getattr(obj1, m2m).add(obj2) |
|---|
| 164 | |
|---|
| 165 | #load = transaction.commit_on_success(load) |
|---|
| 166 | |
|---|
| 167 | |
|---|
| 168 | def load_recursive(objects, obj, hooks): |
|---|
| 169 | """ |
|---|
| 170 | Save the given object into the database. If the object has |
|---|
| 171 | ForeignKey relations to other objects, first make sure they are |
|---|
| 172 | already saved (and repeat recursively). |
|---|
| 173 | """ |
|---|
| 174 | |
|---|
| 175 | meta = obj._meta |
|---|
| 176 | |
|---|
| 177 | if hooks['pre_walk'](obj) is None: |
|---|
| 178 | logging.debug('skipping %s.%s %s' % ( |
|---|
| 179 | meta.app_label, meta.module_name, obj._get_pk_val())) |
|---|
| 180 | return |
|---|
| 181 | |
|---|
| 182 | for field in meta.fields: |
|---|
| 183 | if isinstance(field, models.ForeignKey): |
|---|
| 184 | related_meta = field.rel.to._meta |
|---|
| 185 | related_app = related_meta.app_label |
|---|
| 186 | related_model = related_meta.module_name |
|---|
| 187 | related_pk_val = getattr(obj, field.name+'_id') |
|---|
| 188 | try: |
|---|
| 189 | related_obj = objects.pop((related_app, |
|---|
| 190 | related_model, |
|---|
| 191 | related_pk_val)) |
|---|
| 192 | load_recursive(objects, related_obj, hooks) |
|---|
| 193 | except KeyError: |
|---|
| 194 | logging.debug('probably loaded already: ' |
|---|
| 195 | '%(related_app)s.%(related_model)s ' |
|---|
| 196 | '%(related_pk_val)s' % locals()) |
|---|
| 197 | |
|---|
| 198 | logging.debug('loading %s.%s %s' % ( |
|---|
| 199 | meta.app_label, |
|---|
| 200 | meta.module_name, |
|---|
| 201 | obj._get_pk_val())) |
|---|
| 202 | try: |
|---|
| 203 | if hooks['pre_save'](obj) is None: |
|---|
| 204 | logging.debug('skipping %s.%s %s' % ( |
|---|
| 205 | meta.app_label, meta.module_name, obj._get_pk_val())) |
|---|
| 206 | else: |
|---|
| 207 | obj.save() |
|---|
| 208 | |
|---|
| 209 | except Exception, e: |
|---|
| 210 | logging.error('%s while saving %s.%s %s %s' % ( |
|---|
| 211 | e, meta.app_label, meta.module_name, obj._get_pk_val(), obj)) |
|---|
| 212 | raise |
|---|
| 213 | |
|---|
| 214 | |
|---|
| 215 | if __name__ == '__main__': |
|---|
| 216 | from optparse import OptionParser |
|---|
| 217 | p = OptionParser() |
|---|
| 218 | p.add_option('-s', '--settings' , action='store', help='Set the Django settings file') |
|---|
| 219 | p.add_option('-d', '--dump', action='store_const', const='dump', dest='action', help='Dump all Django objects into a file') |
|---|
| 220 | p.add_option('-l', '--load', action='store_const', const='load', dest='action', help='Load all Django objects from a file') |
|---|
| 221 | p.add_option('-f', '--file', default='djangodb.pickle', help='Specify file path [djangodb.pickle]') |
|---|
| 222 | p.add_option('-p', '--plugin', help='Use .py plugin for preprocessing objects to load') |
|---|
| 223 | p.add_option('-v', '--verbose' , action='store_const', const=logging.DEBUG, dest='loglevel', help='Show verbose output for debugging') |
|---|
| 224 | p.add_option('-q', '--quiet' , action='store_const', const=logging.FATAL, dest='loglevel', help='No output at all') |
|---|
| 225 | (opts, args) = p.parse_args() |
|---|
| 226 | loglevel = opts.loglevel or logging.INFO |
|---|
| 227 | try: |
|---|
| 228 | # Python 2.4+ syntax |
|---|
| 229 | logging.basicConfig(level=loglevel, format='%(levelname)-8s %(message)s') |
|---|
| 230 | except TypeError: |
|---|
| 231 | # Python 2.3 |
|---|
| 232 | logging.basicConfig() |
|---|
| 233 | |
|---|
| 234 | if opts.settings: |
|---|
| 235 | os.environ['DJANGO_SETTINGS_MODULE'] = opts.settings |
|---|
| 236 | |
|---|
| 237 | from django.db import transaction, models, backend |
|---|
| 238 | |
|---|
| 239 | if opts.action == 'dump': |
|---|
| 240 | dump(opts.file) |
|---|
| 241 | elif opts.action == 'load': |
|---|
| 242 | load = transaction.commit_on_success(load) |
|---|
| 243 | load(opts.file, opts.plugin) |
|---|
| 244 | else: |
|---|
| 245 | print 'Please specify --dump or --load' |
|---|