Al fin después de mucho trabajar ya encontramos el arreglo de este enigma que ciertos los lectores de nuestro espacio han presentado. Si deseas compartir algo puedes compartir tu conocimiento.
Solución:
Idea general
Puede utilizar qs.union:
- Crea 2 modelos sin relaciones entre ellos. No olvides usar
class Meta: managed = False
- seleccione desde el primer modelo, anote con subconsulta y unión con el segundo:
from django.db import models
from django.db.models import F, OuterRef, Subquery, Value
from django.db.models.functions import Coalesce
# OperationalDevice fields: ip, mac
# AllowedDevice fields: ip, type, owner
USE_EMPTY_STR_AS_DEFAULT = True
null_char_field = models.CharField(null=True)
if USE_EMPTY_STR_AS_DEFAULT:
default_value = ''
else:
default_value = None
# By default Expressions treat strings as "field_name" so if you want to use
# empty string as a second argument for Coalesce, then you should wrap it in
# `Value()`.
# `None` can be used there without wrapping in `Value()`, but in
# `.annotate(type=NoneValue)` it still should be wrapped, so it's easier to
# just "always wrap".
default_value = Value(default_value, output_field=null_char_field)
operational_devices_subquery = OperationalDevice.objects.filter(ip=OuterRef('ip'))
qs1 = (
AllowedDevice.objects
.all()
.annotate(
mac=Coalesce(
Subquery(operational_devices_subquery.values('mac')[:1]),
default_value,
output_field=null_char_field,
),
)
)
qs2 = (
OperationalDevice.objects
.exclude(
ip__in=qs1.values('ip'),
)
.annotate(
type=default_value,
owner=default_value,
)
)
final_qs = qs1.union(qs2)
Enfoque genérico para múltiples campos
Un enfoque más complejo pero “universal” puede utilizar Model._meta.get_fields()
. Será más fácil de usar en los casos en que el “segundo” modelo tenga más de 1 campo adicional (no solo ip,mac
). Código de ejemplo (no probado, pero da una impresión general):
# One more import:
from django.db.models.fields import NOT_PROVIDED
common_field_name = 'ip'
# OperationalDevice fields: ip, mac, some_more_fields ...
# AllowedDevice fields: ip, type, owner
operational_device_fields = OperationalDevice._meta.get_fields()
operational_device_fields_names = _f.name for _f in operational_device_fields # or set((_f.name for ...))
allowed_device_fields = AllowedDevice._meta.get_fields()
allowed_device_fields_names = _f.name for _f in allowed_device_fields # or set((_f.name for ...))
operational_devices_subquery = OperationalDevice.objects.filter(ip=OuterRef(common_field_name))
left_joined_qs = ( # "Kind-of". Assuming AllowedDevice to be "left" and OperationalDevice to be "right"
AllowedDevice.objects
.all()
.annotate(
**
_f.name: Coalesce(
Subquery(operational_devices_subquery.values(_f.name)[1]),
Value(_f.get_default()), # Use defaults from model definition
output_field=_f,
)
for _f in operational_device_fields
if _f.name not in allowed_device_fields_names
# NOTE: if fields other than `ip` "overlap", then you might consider
# changing logic here. Current implementation keeps fields from the
# AllowedDevice
# Unpacked dict is partially equivalent to this:
# mac=Coalesce(
# Subquery(operational_devices_subquery.values('mac')[:1]),
# default_for_mac_eg_fallback_text_value,
# output_field=null_char_field,
# ),
# other_field = Coalesce(...),
# ...
)
)
lonely_right_rows_qs = (
OperationalDevice.objects
.exclude(
ip__in=AllowedDevice.objects.all().values(common_field_name),
)
.annotate(
**
_f.name: Value(_f.get_default(), output_field=_f), # Use defaults from model definition
for _f in allowed_device_fields
if _f.name not in operational_device_fields_names
# NOTE: See previous NOTE
)
)
final_qs = left_joined_qs.union(lonely_right_rows_qs)
Uso de OneToOneField para “mejor” SQL
Teóricamente puedes usar device_info = models.OneToOneField(OperationalDevice, db_column='ip', primary_key=True, related_name='status_info')
: en AllowedDevice
. En este caso, su primer QS puede definirse sin el uso de Subquery
:
from django.db.models import F
# Now 'ip' is not in field names ('device_info' is there), so add it:
allowed_device_fields_names.add(common_field_name)
# NOTE: I think this approach will result in a more compact SQL query without
# multiple `(SELECT "some_field" FROM device_info_table ... ) as "some-field"`.
# This also might result in better query performance.
honest_join_qs = (
AllowedDevice.objects
.all()
.annotate(
**
_f.name: F(f'device_info___f.name')
for _f in operational_device_fields
if _f.name not in allowed_device_fields_names
)
)
final_qs = honest_join_qs.union(lonely_right_rows_qs)
# or:
# final_qs = honest_join_qs.union(
# OperationalDevice.objects.filter(status_info__isnull=True).annotate(**missing_fields_annotation)
# )
# I'm not sure which approach is better performance-wise...
# Commented one will use something like:
# `SELECT ... FROM "device_info_table" LEFT OUTER JOIN "status_info_table" ON ("device_info_table"."ip" = "status_info_table"."ip") WHERE "status_info_table"."ip" IS NULL
#
# So it might be a little better than first with `union(QS.exclude(ip__in=honest_join_qs.values('ip'))`.
# Because later uses SQL like this:
# `SELECT ... FROM "device_info_table" WHERE NOT ip IN (SELECT ip FROM "status_info_table")`
#
# But it's better to measure timings of both approaches to be sure.
# @GrannyAching, can you compare them and tell in the comments which one is better ?
PS Para automatizar la definición de modelos, puede utilizar manage.py inspectdb
PPS Quizás herencia de múltiples tablas con OneToOneField(..., parent_link=True)
puede ser más útil para usted que usar union
.
Ya que ip
es primario key tanto en la primera tabla se actualiza con frecuencia, sugiero actualizar la segunda tabla y convertir el ip
en la segunda mesa para tener ip
de la primera mesa como OneToOneField
.
Así es como deberían verse sus modelos:
class ModelA(models.Model):
ip = models.GenericIPAddressField(unique=True)
mac = models.CharField(max_length=17, null=True, blank=True)
class ModelB(models.Model):
ip = models.OneToOneField(ModelA)
type = models.CharField()
owner = models.CharField()
docs
También puede tener la relación uno a uno usando una columna separada:
class ModelB(models.Model):
ip = models.GenericIPAddressField(unique=True)
type = models.CharField()
owner = models.CharField()
modelA = models.OneToOneField(ModelA)
Así que ahora puede tener la dirección IP como principal key, y aún puede consultar la tabla ModelA
usando el campo modelA
.
Una vez que tenga un valor de una de ambas tablas, simplemente haga una consulta en la otra, buscando id. Dado que estas dos tablas están separadas, debe realizar una consulta adicional. No es necesario crear una relación explícita, ya que está buscando en su “id / ip”. Entonces, una vez que tenga un primer valor, llamado ‘first_object’, simplemente busque su relativo en la otra tabla.
other_columns = ModelB.objects.get(id=first_object.id)
Luego, si desea simplemente ‘agregar’ las columnas deseadas al otro modelo y enviar un solo objeto a lo que desee:
first_object.attr1 = other_columns.attr1
...
Aquí puedes ver las reseñas y valoraciones de los usuarios
Si te mola el asunto, eres capaz de dejar un escrito acerca de qué le añadirías a esta noticia.