How to find all Django foreign key references to an instance - python

How do you find all direct foreign key references to a specific Django model instance?
I want to delete a record, but I want to maintain all child records that refer to it, so I'm trying to "swap out" the reference to the old record with a different one before I delete it.
This similar question references the Collector class. I tried:
obj_to_delete = MyModel.objects.get(id=blah)
new_obj = MyModel.objects.get(id=blah2)
collector = Collector(using='default')
collector.collect([obj_to_delete])
for other_model, other_data in collector.field_updates.iteritems():
for (other_field, _value), other_instances in other_data.iteritems():
# Why is this necessary?
if other_field.rel.to is not type(first_obj):
continue
for other_instance in other_instances:
setattr(other_instance, other_field.name, new_obj)
other_instance.save()
# All FK references should be gone, so this should be safe to delete.
obj_to_delete.delete()
However, this seems to have two problems:
Sometimes collector.field_updates contains references to models and fields that have nothing to do with my target obj_to_delete.
My final obj_to_delete.delete() call fails with IntegrityErrors complaining about remaining records that still refer to it, records that weren't caught by the collector.
What am I doing wrong?
I just need a way to lookup all FK references to a single model instance. I don't need any kind of fancy dependency lookup like what's used in Django's standard deletion view.

You can use Django's reverse foreign key support.
Say you have two models, like so:
class Foo(models.Model):
name = models.CharField(max_length=10)
class Bar(models.Model):
descr = models.CharField(max_length=100)
foo = models.ForeignKey(Foo)
Then you know you can do bar_instance.foo to access the Foo object it keys to. But you can use the reverse foreign key on a Foo instance to get all the Bar objects that point to it using, e.g, foo.bar_set.

Personally, I think the best option is to avoid the cascaded deletion.
Declaring the foreign keys in the related models with the proper Django option, e.g.
on_delete=models.SET_NULL
should suffice.
Borrowing the sample models from #Joseph's answer:
class Foo(models.Model):
name = models.CharField(max_length=10)
class Bar(models.Model):
descr = models.CharField(max_length=100)
foo = models.ForeignKey(Foo, blank=True, null=True, on_delete=models.SET_NULL))
As described in the official Django docs, here are the predefined behaviours you can use and experiment with:
SET_NULL: Set the ForeignKey null; this is only possible if null is
True.
SET_DEFAULT: Set the ForeignKey to its default value; a default for
the ForeignKey must be set.
SET(): Set the ForeignKey to the value passed to SET(), or if a
callable is passed in, the result of calling it. In most cases, passing a callable will be necessary to avoid executing queries at the time your models.py is imported:
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
def get_sentinel_user():
return get_user_model().objects.get_or_create(username='deleted')[0]
class MyModel(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.SET(get_sentinel_user))
DO_NOTHING: Take no action. If your database backend enforces
referential integrity, this will cause an IntegrityError unless you
manually add an SQL ON DELETE constraint to the database field

Related

Converting a model to its base model

Consider this file :
from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
Now, let's say that I have a Restaurant, named restaurant. But this place is no longer a restaurant, so I want to transform it to a Place. For that, I do :
p = Place.objects.get(pk=place_id)
p.restaurant.delete()
p.save()
It works well, p is no longer a restaurant, but something strange happens : The primary key (ID) of p in the Place table change, like if the Place was deleted and then recreated.
Why is this happening ?
And how can I transform my restaurant to a place without changing the place ID ?
You inherited restaurant from place and Django do some stuff about this kind of relation between two table in DB.Django calls that Multi-table inheritance.
Some of the features of this type of design include the following:
PK of two objects are same.
Two object in Django point of view seems as a one object so every change in PK of one object cause automatic change in other object.
There is an automatically-created OneToOneField relation between two model.
There is atomic transaction for query in these objects.
So this is normal behavior of Django ORM and Django manage change in those two object's PK. You can read more about this concept with concrete model and multi-table inheritance in this link.

Recursive delete foreign keys for Django object

Suppose I have an object called Person that has a foreign key that links to CLothes which links to
class Person(models.Model):
clothes = models.ForeignKey('Clothes', on_delete=models.PROTECT)
jokes = models.ManyToManyField(to='Jokes')
class Clothes(models.Model):
fabric = models.ForeignKey('Material', on_delete=models.PROTECT)
class Material(models.Model):
plant = models.ForeignKey('Plant', on_delete=models.PROTECT)
And if I wanted to delete person, I would have to delete Clothes, Jokes, Materials attached to it. Is there a way to recursively detect all the foreign keys so that I can delete them?
The django.db.models.deletion.Collector is suited for this task. It is what Django uses under the hood to cascade deletions.
You can use it this way:
from django.db.models.deletion import Collector
collector = Collector(using='default') # You may specify another database
collector.collect([some_instance])
for model, instance in collector.instances_with_model():
# Our instance has already been deleted, trying again would result in an error
if instance == some_instance:
continue
instance.delete()
For more information about the Collector class, you can refer to this question:
How to show related items using DeleteView in Django?
As mentioned in the comments, using on_delete=models.CASCADE would be the best solution but if you do not have control over that, this should work.

Django: find all reverse references by foreign keys

Well, now I'm using Django 1.6+
And I have a model:
class FileReference(models.Model):
# some data fields
# ...
pass
class Person(models.Model):
avatar = models.ForeignKey(FileReference, related_name='people_with_avatar')
class House(models.Model):
images = models.ManyToManyField(FileReference, related_name='houses_with_images')
class Document(model.Model):
attachment = models.OneToOneField(FileReference, related_name='document_with_attachment')
So, many other model will have a foreign key referring to the FileReference model.
But sometimes, the referring models is deleted, with the FileReference object left.
I want to delete the FileReference objects with no foreign key referencing.
But so many other places will have foreign keys.
Is there any efficient way to find all the references? i.e. get the reference count of some model object?
I stumbled upon this question and I got a solution for you. Note, that django==1.6 is not supported any more, so this solution will probably work on django>=1.9
Lets say we are talking about 2 of the objects for now:
class FileReference(models.Model):
pass
class Person(models.Model):
avatar = models.ForeignKey(FileReference, related_name='people_with_avatar', on_delete=models.CASCADE)
As you can see in ForeignKey.on_delete documentation, when you delete the related FileReference object, the referenced object Person is deleted as well.
Now for your question. How do we do the revered? We want upon Person deletion that FileReference object will be removed as well.
We will do that using post_delete signal:
def delete_reverse(sender, **kwargs):
try:
if kwargs['instance'].avatar:
kwargs['instance'].avatar.delete()
except:
pass
post_delete.connect(delete_reverse, sender=Person)
What we did there was deleting the reference in avatar field on Person deletion. Notice that the try: except: block is to prevent looping exceptions.
Extra:
The above solution will work on all future objects. If you want to remove all of the past objects without a reference do the following:
In your package add the following file and directories: management/commands/remove_unused_file_reference.py
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
def handle(self, *args, **options):
file_references = FileReference.objects.all()
file_reference_mapping = {file_reference.id: file_reference for file_reference in file_references}
persons = Person.objects.all()
person_avatar_mapping = {person.avatar.id: person for person in persons}
for file_reference_id, file_reference in file_reference_mapping.items():
if file_reference_id not in person_avatar_mapping:
file_reference.delete()
When you done, call: python manage.py remove_unused_file_reference
This is the base idea, you can change it to bulk delete etc...
I hope this will help to someone out there. Good Luck!

Error saving django model with OneToOne field - Column specified twice

This question has been asked before, but the answers there do not solve my problem.
I am using a legacy database, nothing can be changed
Here are my django models, with all but the relevant fields stripped off, obviously class meta has Managed=False in my actual code:
class AppCosts(models.Model):
id = models.CharField(primary_key=True)
cost = models.DecimalField()
class AppDefs(models.Model):
id = models.CharField(primary_key=True)
data = models.TextField()
appcost = models.OneToOneField(AppCosts, db_column='id')
class JobHistory(models.Model):
job_name = models.CharField(primary_key=True)
job_application = models.CharField()
appcost = models.OneToOneField(AppCosts, to_field='id', db_column='job_application')
app = models.OneToOneField(AppDefs, to_field='id', db_column='job_application')
The OneToOne fields work fine for querying, and I get the correct result using select_related()
But when I create a new record for the JobHistory table, when I call save(), I get:
DatabaseError: (1110, "Column 'job_application' specified twice")
I am using django 1.4 and I do not quite get how this OneToOneField works. I can't find any example where primary keys are named differently and has this particular semantics
I need the django model that would let me do this SQL:
select job_history.job_name, job_history.job_application, app_costs.cost from job_history, app_costs where job_history.job_application = app_costs.id;
You have defined appcost and app to have the same underlying database column, job_application, which is also the name of another existing field: so three fields share the same column. That makes no sense at all.
OneToOneFields are just foreign keys constrained to a single value on both ends. If you have foreign keys from JobHistory to AppCost and AppDef, then presumably you have actual columns in your database that contain those foreign keys. Those are the values you should be using for db_field for those fields, not "job_application".
Edit I'm glad you said you didn't design this schema, because it is pretty horrible: you won't have any foreign key constraints, for example, which makes referential integrity impossible. But never mind, we can actually achieve what you want, more or less.
There are various issues with that you have, but the main one is that you don't need the separate "job_application" field at all. That is, as I said earlier, the foreign key, so let it be that. Also note it should be an actual foreign key field, not a one-to-one, since there are many histories to one app.
One constraint that we can't achieve easily in Django is to have the same field acting as FK for two tables. But that doesn't really matter, since we can get to AppCosts via AppDefs.
So the models could just look like this:
class AppCosts(models.Model):
app = models.OneToOneField('AppDefs', primary_key=True, db_field='id')
cost = models.DecimalField()
class AppDefs(models.Model):
id = models.CharField(primary_key=True)
data = models.TextField()
class JobHistory(models.Model):
job_name = models.CharField(primary_key=True)
app = models.ForeignKey(AppDefs, db_column='job_application')
Note that I've moved the one-to-one between Costs and Defs onto AppCosts, since it seems to make sense to have the canonical ID in Defs.
Now, given a JobHistory instance, you can do history.app to get the app instance, history.app.cost to get the app cost, and use the history.app_id to get the underlying app ID from the job_application column.
If you wanted to reproduce that SQL output more exactly, something like this would now work:
JobHistory.objects.values_list('job_name', 'app_id', 'app__appcosts__cost')

How to reference two ForeignKeys in one model

I want to accomplish the following:
I have three classes derived from an abstract class:
class Person(models.Model):
name = models.CharField()
...
class Meta:
abstract = True
class TypA(Person):
...
class TypB(Person):
...
class TypC(Person):
...
In another class I would like to reference TypA and TypB as a Foreign Key, something like this:
class Project(models.Model):
worker = models.ForeignKey(TypA or TypB)
Since it is not possible to declare two different models as a Foreign Key I am on the look for solutions.
I read about Generic Foreign Keys; but I am unsure how to apply that to my model.
Another idea is to use the limit_choices_to declaration for ForeignKeys.
worker = models.ForeignKey(Person, limit_choices_to={??})
But this is not possible as it seems:
Field defines a relation with model 'Person', which is either not installed, or is abstract.
Thank you in advance for the help.
A Django ForeignKey field translates to a database foreign key. Your Person model is abstract, so that one doesn't exist in the database, so there can be no foreign keys to that one.
Likewise a database foreign key can only reference one table, not two.
If you really want a flexible relation to more than one kind of table, the only possibility I see is Django's contenttypes framework.
You also want to limit the kinds of models you can point at. For that you'd best look at How can I restrict Django's GenericForeignKey to a list of models? for an example.
you just need to reference your abstract class(like JAVA):
class Project(models.Model):
worker = models.ForeignKey(Person)
#in your code:
worker = TypeA()
worker.save()
proj = Project()
proj.worker = worker
proj.save()

Categories

Resources