Объединить две несвязанные таблицы / модели с одним и тем же первичным ключом в Django - PullRequest
7 голосов
/ 12 апреля 2019

У меня есть две несвязанные таблицы с одним и тем же первичным ключом.

ip            mac
11.11.11.11   48-C0-09-1F-9B-54
33.33.33.33   4E-10-A3-BC-B8-9D
44.44.44.44   CD-00-60-08-56-2A
55.55.55.55   23-CE-D3-B1-39-A6

ip            type     owner
22.22.22.22   laptop   John Doe
33.33.33.33   server   XYZ Department
44.44.44.44   VM       Mary Smith
66.66.66.66   printer  ZWV Department

Первая таблица автоматически обновляется каждую минуту.Я не могу изменить структуру базы данных или сценарий, который ее заполняет.

Обе таблицы имеют ip как ПЕРВИЧНЫЙ КЛЮЧ.

В представлении я хотел бы отобразить таблицу, подобную этой:

ip           mac               type    owner          Alert
11.11.11.11  48-C0-09-1F-9B-54                        Unauthorized
55.55.55.55  23-CE-D3-B1-39-A6                        Unauthorized
22.22.22.22                    laptop  John Doe       Down
66.66.66.66                    printer ZWV Department Down
33.33.33.33  4E-10-A3-BC-B8-9D server  XYZ Department OK
44.44.44.44  CD-00-60-08-56-2A VM      Mary Smith     OK

Как мне это смоделировать?Должен ли я сделать один из двух первичных ключей внешним ключом для другого?

Как только код заработает, будет много данных, поэтому я хочу убедиться, что он достаточно быстрый.

Какой самый быстрый способ получения данных?


Обновление:

Я пытался использовать OneToOneField для второй таблицы.

Это помогает мне получить записи, которые находятся в обеих таблицах, и записи для неавторизованных устройств (IP-адреса отсутствуют во второй таблице):

ip           mac               type    owner          Alert
11.11.11.11  48-C0-09-1F-9B-54                        Unauthorized
55.55.55.55  23-CE-D3-B1-39-A6                        Unauthorized
33.33.33.33  4E-10-A3-BC-B8-9D server  XYZ Department OK
44.44.44.44  CD-00-60-08-56-2A VM      Mary Smith     OK

, но я не могу получить устройства, которые не работают (IP-адресаотсутствует в первой таблице):

22.22.22.22                    laptop  John Doe       Down
66.66.66.66                    printer ZWV Department Down

Я попросил помощи здесь , но, кажется, это невозможно сделать с OneToOneField

Ответы [ 3 ]

5 голосов
/ 03 мая 2019

Общая идея

Вы можете использовать qs.union :

  • создать 2 модели без каких-либо связей между ними.Не забудьте использовать class Meta: managed = False
  • , выбрать из первой модели, аннотировать подзапросом и объединить со вторым:
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)

Общий подход для нескольких полей

Более сложный, но "универсальный" подход может использовать Model._meta.get_fields().Это будет легче использовать в случаях, когда «вторая» модель имеет более 1 дополнительного поля (не только ip,mac).Пример кода (не тестировался, но дает общее впечатление):

# 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)

Использование OneToOneField для «лучшего» SQL

Теоретически вы можете использовать device_info = models.OneToOneField(OperationalDevice, db_column='ip', primary_key=True, related_name='status_info'): in AllowedDevice.В этом случае ваш первый QS может быть определен без использования 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. Для автоматизации определения моделей вы можете использовать manage.py inspectdb

PPS Возможно наследование нескольких таблиц с пользовательским OneToOneField(..., parent_link=True) может быть более полезным для вас, чем использование union.

4 голосов
/ 12 апреля 2019

Поскольку ip является первичным ключом, и первая таблица часто обновляется, я предлагаю обновить вторую таблицу и преобразовать ip во второй таблице, чтобы ip первой таблицы был * 1004.*.

Вот так должны выглядеть ваши модели:

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()

документы

Вы также можете иметь отношение один к одному, используяотдельный столбец:

class ModelB(models.Model):
    ip = models.GenericIPAddressField(unique=True) 
    type = models.CharField()
    owner = models.CharField()
    modelA = models.OneToOneField(ModelA)

Так что теперь вы можете иметь IP-адрес в качестве первичного ключа, и вы все равно можете ссылаться на таблицу ModelA, используя поле modelA.

3 голосов
/ 02 мая 2019

Как только вы получите значение из одной из двух таблиц, просто сделайте запрос в другую, ища идентификатор.Поскольку эти две таблицы разделены, вы должны сделать дополнительный запрос.Вам не нужно создавать явное отношение, так как вы просматриваете его "id / ip".Поэтому, когда у вас есть первое значение с именем 'first_object', просто найдите его родственник в другой таблице.

other_columns = ModelB.objects.get(id=first_object.id)

Тогда, если вы хотите просто «добавить» нужные столбцы в другую модель и отправить один объект к тому, что вы хотите:

first_object.attr1 = other_columns.attr1
...
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...