Saltar al contenido

Combinar dos tablas / modelos no relacionados con el mismo primario key en Django

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.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *