OneToOneField caching behavior

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 Addresses 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 Addresses) 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.