Получить самую новую запись партнера для каждого типа сообщения таблицы NAST?

Вопрос общий (больше не после правок ...), потому что я понимаю, что это обычная проблема и для других таблиц, но я опишу свою конкретную проблему с выбором партнеров для выходных сообщений.

Для данного счета я хочу связать партнера с каждым типом сообщения в NAST таблице. Для одного и того же типа сообщения может быть несколько записей, поэтому мне нужна самая новая на основе полей ERDAT и ERUHR (дата и время).

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

Затем я реализовал это решение, но оно мне не нравится, и я надеялся на что-то получше.

DATA: lt_msg_type_rg TYPE RANGE OF kschl.

lt_msg_type_rg = VALUE #( FOR ls_msg_type IN me->mt_message_type 
                          ( sign = 'I' option = 'EQ' low = ls_msg_type-kschl ) ).
SELECT FROM nast AS invoice_msg_status
      FIELDS invoice_msg_status~kschl AS message_type,
             invoice_msg_status~parnr AS partner_num,
             CONCAT( invoice_msg_status~erdat, invoice_msg_status~eruhr ) AS create_timestamp
      WHERE invoice_msg_status~kappl  = @c_app_invoicing
        AND invoice_msg_status~objky  = @me->m_invoice_num
        AND invoice_msg_status~kschl IN @lt_msg_type_rg
      ORDER BY create_timestamp DESCENDING
      INTO TABLE @DATA(lt_msg_partner).

DATA: lt_partner_rg TYPE RANGE OF parnr.

LOOP AT lt_msg_partner ASSIGNING FIELD-SYMBOL(<lgr_msg_partner>) GROUP BY <lgr_msg_partner>-message_type.
  lt_partner_rg = COND #( WHEN line_exists( lt_partner_rg[ low = <lgr_msg_partner>-partner_num ] )
                          THEN lt_partner_rg
                          ELSE VALUE #( BASE lt_partner_rg ( sign = 'I' option = 'EQ' low = <lgr_msg_partner>-partner_num ) ) ).
ENDLOOP.

Пример ввода (пропущены нерелевантные поля)

+-------+-------+-------+-------+------------+-------+
| KAPPL | OBJKY | KSCHL | PARNR |   ERDAT    | ERUHR |
+-------+-------+-------+-------+------------+-------+
| V3    | 12345 | Z001  |    11 | 27.10.2020 | 11:00 |
| V3    | 12345 | Z001  |    12 | 27.10.2020 | 12:00 |
| V3    | 12345 | Z002  |    13 | 27.10.2020 | 11:00 |
+-------+-------+-------+-------+------------+-------+

Ожидаемый результат:

[12]
[13]

person RaTiO    schedule 27.10.2020    source источник


Ответы (3)


К сожалению, SQL не предоставляет простого синтаксиса для этого довольно распространенного вида выбора. Решения всегда будут включать несколько последующих или вложенных выборок.

Согласно вашему описанию, я предполагаю, что вы уже нашли оператор ABAP SQL с глубокой вложенностью «сделай все в одном», но он вас не удовлетворяет, потому что читаемость слишком сильно страдает.

В подобных случаях мы часто прибегаем к процедурам базы данных, управляемым ABAP (AMDP). Они позволяют разложить сложные вложенные выборки на серию простых последующих выборок.

CLASS cl_read_nast DEFINITION
    PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.

    INTERFACES if_amdp_marker_hdb.

    TYPES:
      BEGIN OF result_row_type,
        parnr TYPE char2,
      END OF result_row_type.

    TYPES result_table_type
      TYPE STANDARD TABLE OF result_row_type
        WITH EMPTY KEY.

    TYPES:
      BEGIN OF key_range_row_type,
        kschl TYPE char4,
      END OF key_range_row_type.

    TYPES key_range_table_type
      TYPE STANDARD TABLE OF key_range_row_type
        WITH EMPTY KEY.

    CLASS-METHODS select
      IMPORTING
        VALUE(application)  TYPE char2
        VALUE(invoice_number)  TYPE char5
        VALUE(message_types)  TYPE key_range_table_type
      EXPORTING
        VALUE(result) TYPE result_table_type.

ENDCLASS.

CLASS cl_read_nast IMPLEMENTATION.

  METHOD select
      BY DATABASE PROCEDURE FOR HDB LANGUAGE SQLSCRIPT
      USING nast.

    last_changed_dates =
      select kappl, objky, kschl,
          max( erdat || eruhr ) as last_changed_on
        from nast
        where kappl = :application
          and objky = :invoice_number
          and kschl in
            ( select kschl from :message_types )
        group by kappl, objky, kschl;

    last_changers =
      select nast.kschl,
          max( nast.parnr ) as parnr
        from nast
        inner join :last_changed_dates
          on nast.kappl = :last_changed_dates.kappl
          and nast.objky = :last_changed_dates.objky
          and nast.kschl = :last_changed_dates.kschl
          and nast.erdat || nast.eruhr = :last_changed_dates.last_changed_on
        group by nast.kschl;

    result =
      select distinct parnr
        from :last_changers;

  ENDMETHOD.

ENDCLASS.

Проверено с помощью следующего интеграционного теста:

CLASS integration_tests DEFINITION
    FOR TESTING RISK LEVEL CRITICAL DURATION SHORT.

  PRIVATE SECTION.

    TYPES db_table_type
      TYPE STANDARD TABLE OF nast
        WITH EMPTY KEY.

    CLASS-METHODS class_setup.

    METHODS select FOR TESTING.

ENDCLASS.

CLASS integration_tests IMPLEMENTATION.

  METHOD class_setup.

    DATA(sample) =
      VALUE db_table_type(
        ( kappl = 'V3' objky = '12345' kschl = 'Z001' parnr = '11' erdat = '20201027' eruhr = '1100' )
        ( kappl = 'V3' objky = '12345' kschl = 'Z001' parnr = '12' erdat = '20201027' eruhr = '1200' )
        ( kappl = 'V3' objky = '12345' kschl = 'Z002' parnr = '13' erdat = '20201027' eruhr = '1100' ) ).

    MODIFY nast
      FROM TABLE @sample.

    COMMIT WORK AND WAIT.

  ENDMETHOD.

  METHOD select.

    DATA(invoicing) = 'V3'.

    DATA(invoice_number) = '12345'.

    DATA(message_types) =
      VALUE zcl_fh_read_nast=>key_range_table_type(
        ( kschl = 'Z001' )
        ( kschl = 'Z002' ) ).

    cl_read_nast=>select(
      EXPORTING
        application = invoicing
        invoice_number = invoice_number
        message_types = message_types
      IMPORTING
        result = DATA(actual_result) ).

    DATA(expected_result) =
      VALUE cl_read_nast=>result_table_type(
        ( parnr = '12' )
        ( parnr = '13' ) ).

    cl_abap_unit_assert=>assert_equals(
        act = actual_result
        exp = expected_result ).

  ENDMETHOD.

ENDCLASS.
person Florian    schedule 27.10.2020
comment
Спасибо. На самом деле я не тестировал единственный пример глубокого SQL. Я заметил, что второй вложенный подзапрос, вероятно, был невозможен, и в любом случае он начал очень плохо выглядеть с повторяющимися условиями WHERE. Ваше решение выглядит действительно хорошо, но кажется излишне спроектированным для моего единственного варианта использования. В случае перехода в этом направлении создания новых объектов, не лучше ли было бы создать представление CDS вместо AMDP? Я всегда стараюсь отдавать предпочтение CDS, а не AMDP, но это может быть просто потому, что мне больше нравится первый. - person RaTiO; 27.10.2020
comment
Я не пробовал, но с CDS, вероятно, необходимо 2, 1-й для создания отметки времени от даты + времени и 2-й, чтобы получить отметку времени MAX для каждого типа сообщения - person RaTiO; 27.10.2020
comment
Я не считаю это чрезмерным. Он добавляет единственный метод с ясной целью. Если вы хотите избежать появления новых объектов разработки, вы даже можете добавить его к существующему классу, AMDP не нужно разделять на собственные классы. Я опубликовал вариант CDS для хорошей меры; он распределяет эту единственную цель на 3 объекта разработки, что не совсем упрощает задачу. - person Florian; 28.10.2020
comment
Хорошо, я отметил это как правильный ответ, потому что для общих целей и повторного использования, вероятно, лучший, но все 3 предоставленных ответа: обработка ABAP, решение CDS и AMDP верны. Но я думаю, что этот вопрос иллюстрирует, как ABAP SQL затрудняет выполнение такой простой общей задачи. - person RaTiO; 28.10.2020

Прежде всего, ваша часть неверна, потому что вы проверяете существование (дедупликацию) только по номеру партнера, и потенциально один и тот же партнер может обслуживать разные типы сообщений, по крайней мере, в моем наборе данных в моей тестовой системе я вижу такие строки. Так что вы также должны проверять по типу сообщения. Группировка цикла по типу сообщения и дедупликация по номеру партнера не имеет смысла, поскольку вы удаляете допустимых партнеров, что встречается в разных типах. Тебе нужно:

SELECT 
....
ORDER BY message_type, create_timestamp DESCENDING
....

Таким образом, ваша группировка LOOP может быть упрощена до этих двух строк:

DELETE ADJACENT DUPLICATES FROM lt_msg_partner COMPARING message_type.
lt_partner_rg = VALUE #( BASE lt_partner_rg FOR GROUPS value_no OF <line_no> IN lt_msg_partner GROUP BY ( partner_num = <line_no>-partner_num ) WITHOUT MEMBERS ( sign = 'I' option = 'EQ' low = value_no-partner_num ) ).
person Suncatcher    schedule 27.10.2020
comment
Ваш код более читабельный, согласитесь. Но, на мой взгляд, результат такой же, как и у меня. Я сортирую по метке времени по убыванию, а затем по LOOP I GROUP BY типу_сообщения, поскольку внутри я не LOOP AT GROUP, он читает первую запись для каждой, которая является самой последней. COND предназначен только для того, чтобы не добавлять одного и того же партнера 2 раза, если он присутствует в 2 типах сообщений. - person RaTiO; 27.10.2020
comment
The COND is only to not append the same partner 2 times if it's present in 2 message types если это было преднамеренно, тогда хорошо, но это требование не было описано в вашем вопросе. Обновлена ​​вторая строка для выполнения этого условия. - person Suncatcher; 28.10.2020

Как предлагается в комментариях к ответу по варианту AMDP, это также можно сделать с помощью представлений CDS.

Во-первых, нам нужно представление, которое отмечает данные:

@AbapCatalog.sqlViewName: 'timednast'
define view timestamped_nast as select from nast {
    kappl,
    objky,
    kschl,
    parnr,
    concat(erdat, eruhr) as timestamp
}

Во-вторых, поскольку синтаксис CDS не позволяет ставить метки времени и группировать в одном представлении, нам нужно другое представление, которое вычисляет даты последних изменений для каждого типа сообщения:

@AbapCatalog.sqlViewName: 'lchgnast'
define view last_changed_nast as
select from timestamped_nast {
    kappl,
    objky,
    kschl,
    max(timestamp) as last_changed_on
} group by kappl, objky, kschl

В-третьих, нам нужно выбрать номера партнеров, связанные с этими временными точками:

@AbapCatalog.sqlViewName: 'lchbnast'
define view last_changers_nast as
  select from last_changed_nast
  inner join timestamped_nast 
    on timestamped_nast.kappl = last_changed_nast.kappl
    and timestamped_nast.objky = last_changed_nast.objky
    and timestamped_nast.kschl = last_changed_nast.kschl
    and timestamped_nast.timestamp = last_changed_nast.last_changed_on
{
    timestamped_nast.kappl,
    timestamped_nast.objky,
    timestamped_nast.kschl,
    parnr
}

SELECT на последнем просмотре last_changers_nast, включая критерии выбора на kappl, objky и kschl, создаст список последних изменений.

Я не уверен насчет ключей таблицы nast. Третье представление предполагает, что не будет двух записей с точно идентичными временными метками для одного объекта. Если это не так, третье представление должно добавить еще одну агрегацию, используя max(parnr) вместо parnr.

person Florian    schedule 28.10.2020