root/trunk/dbpickle/dbpickle.py

Revision 227, 8.9 kB (checked in by akaihola, 2 years ago)

[dbpickle] Added a little error handling in the initial object deletion loop.

Line 
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"""
26dbpickle.py is a Django tool for saving all database objects into a
27file or loading and storing them back into the database.  It's useful
28for migrating from one database engine to another.
29
30This version handles ForeignKeys which refer to the related model's
31primary key.  ManyToMany relations are now supported, but OneToOne
32relations are not.
33
34You can also use you own plugin for pre-processing objects before
35saving them into the database.  This is useful e.g. when a model has
36changed and you're importing objects from an old version of the
37database.  The plugin should be a Python program with a
38``pre_save_hook(obj)`` and/or ``pre_walk_hook(obj)`` function which
39returns True if it modified the object, otherwise False.
40
41Be careful with objects automatically inserted in your app e.g. in save()
42methods.  Those could mess up ID numbering in databases which use sequence
43tables.
44
45
46Usage example
47=============
48
49Let's suppose your project is using PostgreSQL and the database has
50been populated using the app.
51
52$ PYTHONPATH=/home/myproject DJANGO_SETTINGS_MODULE=settings \
53  ./dbpickle.py --dump --file=myproject.pickle
54
55At this point you could change the settings so that SQLite is used
56instead 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
66This would effectively convert your database from PostgreSQL to SQLite
67through Django.
68"""
69
70import cPickle
71import logging
72from imp import load_source
73import os
74
75try:
76    set # Only available in Python 2.4+
77except NameError:
78    from sets import Set as set # Python 2.3 fallback
79
80def 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
117def 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
168def 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
215if __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'
Note: See TracBrowser for help on using the browser.