Hi, the application I’m working on has some model relations that used a nullable ForeignKey to a related model, analogous to this example:
class Address(Model):
company = ForeignKey('Company', null=True, blank=True, related_name='addresses')
class Company(Model):
def create_address(self):
with transaction.atomic():
assert self.addresses.count() == 0
Address.objects.create(company=self)
def do_something_with_address(self):
if self.addresses.count():
do_something(self.addresses.get())
def test_create_address_for_company():
company = mock_company()
assert company.addresses.count() == 0
company.create_address()
address = company.addresses.get()
(aside: I realize that using count()
and get()
in do_something_with_address()
results in an unnecessary extra query, and that this could be written using try/except around get
, but I’m leaving that out of the example for clarity. Also on looking at this again I realize that the transaction.atomic
in the example doesn’t achieve anything and we’d need to do a select_for_update
to guarantee uniqueness of Address
es by company if we don’t have a unique constraint on address.company_id
. But all of that is beside the point.)
We realized we could better model our domain if we explicitly made this relationship unique (every Company
in this example would have zero or one Address
es) so added unique=True
to the foreign key, and everything continued to work nicely, except that Django now loudly warns at application startup that we really should be using OneToOneField
instead of ForeignKey(unique=True)
.
Today I tried migrating this relationship to use OneToOneField
, using hasattr
to suppress the DoesNotExist
that accessing the address
field would otherwise raise, like so:
class Address(Model):
company = OneToOneField('Company', null=True, blank=True)
class Company(Model):
def create_address(self):
with transaction.atomic():
assert not hasattr(self, 'address')
Address.objects.create(company=self)
def do_something_with_address(self):
if hasattr(self, 'address'):
do_something(self.address)
def test_create_address_for_company():
company = mock_company()
assert not hasattr(company, 'address')
company.create_address()
address = company.address
I was surprised to find that the test failed, and looked at the test in a debugger to see what happened. The create_address
call succeeded, and an Address
associated with the test Company
existed in the database, but it appears that the hasattr
call at the beginning of the test caused the ReverseOneToOneDescriptor
representing the company.address
attribute to cache the lack of a related object. Specifically, this call to self.related.get_cached_value
returned None
, causing the Company
instance to continue to behave as if it wasn’t associated with an Address
.
I can see how there are circumstances where this is the desired behavior – you don’t want to have to hit the database every single time you access the attribute here – but I’m having trouble figuring out how to work around this caching behavior. In the original code with ForeignKey
it was clear when the database was being queried: the example test above, as well as any code inside Company
that needed to access the related object, would call get()
on the addresses
RelatedManager. But with OneToOneField
, the actual relation/queryset/manager is abstracted away and inaccessible (as far as I can tell) from code that uses the Company
model.
How can I access the reverse side of a OneToOneField in a way that guarantees I’m getting the actual value in the database as opposed to a value that may have been cached at some earlier point? Do I need to do something like company.refresh_from_db()
in this example? If so, that also seems suboptimal – I don’t care about refreshing the values of the fields in the company
database table, I just want to get the latest value from the related address
table. Are there ways of working around this that I’m not seeing?
Thanks for reading and please let me know if there’s something I’m missing here.