Автоматическое сопоставление Jooq для отношения 1:1, содержащего отношение 1:N

У меня есть следующие DTO:

  • ПродуктDTO

  • ПользовательDTO

  • ПользовательРолеДТО:

  • Мне нужно получить список продуктов, и каждый из этих продуктов имеет поле «creatorId» (NULLABLE).

    • Для идентификатора создателя мне нужно получить соответствующего пользователя (отношение 1:1),
      • и для этого пользователя мне нужно получить его роли (отношения 1:N)

Продукт -> 1:1 -> Пользователь -> 1:N -> Роли пользователя


Я получил это для работы со следующим запросом jooq:

dsl()
    .select(
            asterisk(),
            multiset(
                    selectDistinct(
                            asterisk()
                    )
                            .from(UserRole.USER_ROLE)
                            .where(UserRole.USER_ROLE.USERID.eq(Product.PRODUCT.CREATORID))
            ).as("roles")),
    .from(Product.PRODUCT)
        .leftJoin(User.USER)
        .on(User.USER.USERID.eq(Product.PRODUCT.CREATORID))
    .where(...)
    .groupBy(Product.PRODUCT.PRODUCTID)
    .orderBy(...)
    .offset(...)
    .limit(...)
    .fetch().map(new RecordMapper<Record, ProductDTO>() {
        @Override
        public @Nullable ProductDTO map(Record record) {
            ProductDTO product = rec.into(ProductDTO.class);
            UserDTO creator = rec.into(UserDTO.class);
            List<UserRoleDTO> creatorRoles = rec.get("roles", Result.class).into(UserRoleDTO.class);
            creator.setRoles(creatorRoles);
            product.setCreator(creator);

            return product;
        }
    });

Теперь мой вопрос:

возможно ли это сопоставление «неявно» без явной части «RecordMapper», поэтому сопоставление можно использовать повторно с меньшим количеством шаблонов для разных операторов (fetch, fetchCount, fetchStream)? <-- (также возможно в этом примере, но RecordMapper необходимо снова определить для каждого оператора)

Я уже удовлетворен тем, что заставил это работать, так как я нашел примеры использования мультимножеств только с прямыми отношениями 1:N, но здесь у меня есть отношение 1:1, которое (на более глубоком уровне) также содержит отношение 1:N.


Обычно можно просто создать два отдельных оператора SELECT, но для моего варианта использования (удаленное разбиение на страницы с фильтрацией) необходимо, чтобы все! отношения обрабатываются в одном операторе SELECT, поэтому я могу использовать операторы соединения для дополнительной фильтрации запроса.

Почему бы не использовать рекомендуемый подход с использованием специальных конвертеров?

Lukas Eder 07.06.2024 17:59

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

funkrusher 08.06.2024 23:19

Еще раз спасибо за последовательные ответы! очень ценю, и я многому научился в процессе :)

funkrusher 08.06.2024 23:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
71
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

С помощью Лукаса Эдера (см. выше) и связанной страницы с примерами для специальных преобразователей.


  • Конвертация сделана раньше! выполнение утверждения, как желательно в вопросе.
  • Преобразование на самом деле не является неявным, поскольку специальный преобразователь должен быть настроен для конкретного варианта использования (обнуляемое отношение 1:1 с подсписком 1:N)

Таким образом, этот код можно повторно использовать для разных вариантов использования (fetch(), fetchStream(),...) без необходимости переобъявлять преобразователь для результатов этих различных функций вариантов использования, но преобразование можно централизовать вместе. с этим заявлением DSL для повторного использования.

SelectFinalStep<? extends Record> selectStep = dsl()
    .select(
        asterisk(),
        multiset(
            selectDistinct(
                asterisk(),
                multiset(
                    selectDistinct(
                        asterisk()
                    )
                    .from(UserRole.USER_ROLE)
                    .where(UserRole.USER_ROLE.USERID.eq(User.USER.USERID))
                ).as("roles")
            )
            .from(User.USER)
            .where(User.USER.USERID.eq(Product.PRODUCT.CREATORID))
        ).as("creator").convertFrom(r -> {
            // multiset always gives us a list-result, 
            // but we expect a nullable 1:1 relationship here for sure...
            if (r.isEmpty()) {
                return (UserDTO) null;
            } else {
                Record first = r.getFirst();
                List<UserRoleDTO> roles = first.get("roles", Result.class).into(UserRole.USER_ROLE).into(UserRoleDTO.class);
                return first.into(User.USER).into(UserDTO.class).setRoles(roles);
            }
        })
    )
    .from(Product.PRODUCT)
    .leftJoin(User.USER)
    .on(User.USER.USERID.eq(Product.PRODUCT.CREATORID))
    .where(...)
    .groupBy(Product.PRODUCT.PRODUCTID)
    .orderBy(...)
    .offset(...)
    .limit(...);

// case1 - selecting
selectStep.fetchInto(ProductDTO.class);

// case2 - counting
dsl().fetchCount(selectStep);

// case3 - streaming
selectStep.fetchStreamInto(ProductDTO.class);

Почему бы не применить преобразование на каждом уровне вложенности? 1) вам никогда не нужно использовать asterisk(), 2) вам никогда не нужно использовать литералы .class и отражение. Это можно сделать полностью типобезопасным способом.

Lukas Eder 09.06.2024 11:31

Что касается подсчета, то зачем это нужно? Пагинация? Рассматривали ли вы возможность использования оконной функции для вычисления этого значения счетчика непосредственно с самим запросом, чтобы избежать дополнительных обращений туда и обратно? Пример смотрите в этой статье

Lukas Eder 09.06.2024 11:46

1) я использую asterisk(), потому что DTO со временем часто меняются… мы работаем в SCRUM-спринтах и ​​очень часто добавляем/удаляем функции. Код часто меняется. С помощью asterisk() все поля в DTO сопоставляются автоматически. В противном случае разработчикам придется изменить все места, где поля использовались при выборе, когда поле добавляется/изменяется/удаляется посредством миграции схемы базы данных. Если в этом подходе есть что-то изначально неправильное, я был бы рад узнать об этом, но asterisk() мне показался довольно удобным, и, конечно, часто не все поля необходимы постоянно, но в большинстве наших случаев они необходимы.

funkrusher 09.06.2024 21:32

в моем примере я попробовал применить преобразование на каждом вложенном уровне. Но когда я применил преобразование к as("roles"), я получил ошибку во время выполнения SQL, которую я не смог устранить. Кажется, это работало только тогда, когда я применял преобразование только к мультимножеству «создатель», а не к мультимножеству «роли».

funkrusher 09.06.2024 21:35

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

funkrusher 09.06.2024 21:36

2) Я использовал Pojo, сгенерированные Jooq, в качестве источника для своих DTO, но удалил конструкторы... возможно, мне также следует скопировать их, когда они могут помочь с безопасностью: D

funkrusher 09.06.2024 22:20

Что ж, что по своей сути «неправильно», так это 1) то, что вы в конечном итоге будете проецировать слишком много данных, и это будет очень сложно исправить позже в ваших циклах разработки, так что здесь, по крайней мере, ждет проблема с производительностью. во многих случаях (хотя вы сказали, что не в вашем). 2) asterisk будет генерировать порядок столбцов, указанный в (производственной) базе данных, в отличие от того, что генератор кода jOOQ мог ожидать во время компиляции, поэтому с этим могут быть связаны редкие, тонкие ошибки. Возможно, лучший проект Table.fields() или просто Table напрямую.

Lukas Eder 10.06.2024 09:09

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

Lukas Eder 10.06.2024 09:11

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

funkrusher 10.06.2024 10:28

Давайте продолжим обсуждение в чате.

funkrusher 10.06.2024 16:30
Ответ принят как подходящий

Некоторые общие практические правила:

  • Вам почти никогда не придется использовать оператор asterisk().
  • Вам почти никогда не придется использовать литералы .class в ваших картографах (которые используют отражение, что не является типобезопасным). Конечно, вы все еще можете это сделать, в принципе это поддерживается, но тогда вы не будете знать, правильно ли вы написали запрос и сопоставление, пока не запустите его.
  • Вам почти никогда не придется вручную извлекать значения из результатов или устанавливать значения в DTO.

Не правда ли, это выглядит намного проще?

dsk().select(

         // Project the PRODUCT fields
         PRODUCT.ID,
         PRODUCT.NAME,
         ...

         // Nest a projection of USER fields (using implicit joins)
         row(PRODUCT.user().ID, PRODUCT.user().NAME, ...).mapping(UserDTO::new),

         // Nest a collection of USER_ROLE fields (using join path correlation)
         multiset(
             select(
                 PRODUCT.user().role().ID, 
                 PRODUCT.user().role().NAME, ...)
             .from(PRODUCT.user().role())
         ).convertFrom(r -> r.map(Records.mapping(UserRoleDTO::new))))
    .from(PRODUCT)
    .where(...)
    .orderBy(...)
    .offset(...)
    .limit(...)
    .fetch(Records.mapping(ProductDTO::new));

Предполагается, что вы проектируете свои DTO следующим образом, например:

record UserRoleDTO(long id, String name, ...) {}
record UserDTO(long id, String name, ...) {}
record ProductDTO(long id, String name, UserDTO creator, List<UserRoleDTO> roles) {}

Для простоты вышеприведенное использует

Обе эти функции действительно упрощают эти MULTISET запросы, хотя они, очевидно, не являются обязательными. Если вы предпочитаете, вы все равно можете использовать явные соединения.

Повторное использование запроса для значения COUNT(*)

Вместо выполнения дополнительного обхода для подсчета результатов запроса вместо этого есть возможность использовать оконные функции для вычисления значения COUNT(*) в одном запросе.

Хотя, учитывая, что значение COUNT(*) не зависит от всей вложенности USER и USER_ROLE, возможно, в любом случае лучше выполнить 2 отдельных запроса, где запрос COUNT(*) опускает все вложенные данные, в зависимости от того, как именно вы собираетесь представлять нумерацию страниц.

Сейчас я воспроизвел ваш пример без каких-либо ошибок, но, к сожалению, во время выполнения я получаю ошибку времени выполнения. Когда я удаляю часть row(), ошибка исчезает, и запрос выполняется успешно. set @@group_concat_max_len = @t;]; (conn=1879) You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '.userId, alias_2438556.clientId as v8.clientId, alias_2438556.email a...' at line 12

funkrusher 09.06.2024 22:14

я упростил пример настолько, насколько мог, чтобы по-прежнему выдавать ошибку. Ниже приведен сокращенный пример, который все еще выдает ошибку во время выполнения: dsl().select(PRODUCT.PRODUCTID, row(PRODUCT.fk_product_creatorId().USERID)) .from(Product.PRODUCT);

funkrusher 09.06.2024 22:51

@funkrusher: Кажется, это не имеет отношения к делу, и с этим вам сложно помочь в разделе комментариев здесь. Не могли бы вы задать новый вопрос со всеми необходимыми подробностями?

Lukas Eder 10.06.2024 09:07

Привет, Лукас! Что касается упомянутых здесь «практических правил», можете ли вы сказать мне, можно ли им следовать/применять только в том случае, если таблица в запросе содержит максимум 22 столбца? Мне кажется, что как только таблица содержит больше столбцов, чем 22, типобезопасный подход больше не применим, и тогда можно/должно выбрать другой подход (или провести рефакторинг таблицы)

funkrusher 27.06.2024 16:43

@funkrusher: Это практические правила, поэтому всегда есть исключения. Скорее всего, при больших прогнозах вы будете объединять атрибуты, которые принадлежат друг другу, так что вы все равно сможете оставаться ниже предела в 22 с точки зрения jOOQ. Но в конечном итоге многие дороги ведут в Рим. Делайте то, что лучше всего подходит для вас.

Lukas Eder 27.06.2024 16:55

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

dsl()
    .select(
        row(
            // Project the PRODUCT table
            PRODUCT,

            // USER fields and corresponding ROLES via join path (1:1 containing 1:N)
            row(
                PRODUCT.creator(),
                multiset(
                    select(
                        PRODUCT.creator().user_role()
                    ).from(PRODUCT.creator().user_role())
                ).convertFrom(r -> r.map(RoleDTO::create))
            ).convertFrom(UserDTO::createOrNull),

            // PRODUCT_LANG 1:N via join path
            multiset(
                select(
                    PRODUCT.product_lang()
                ).from(PRODUCT.product_lang())
            ).convertFrom(r -> r.map(ProductLangDTO::create))
        ).convertFrom(ProductDTO::create)
    )

В этом подходе я использую тот факт, что jOOQ автоматически сопоставляет определения «Таблицы» внутри блока «row()» с соответствующими (сгенерированными кодом) классами записей. Единственное, чего тогда не хватает, - это сопоставить эти классы записей с классами DTO, для которых могут быть определены конструкторы/функции внутри DTO, которые создают DTO из RecordN.

Например. Конструкторы/функции для примера следующие:

ПродуктDTO:

    public static ProductDTO create(
        Record3<ProductRecord, UserDTO, List<ProductLangDTO>> r
    ) {
        ProductDTO product = new ProductDTO();
        product.from(r.value1());
        product.setCreator(r.value2());
        product.setLangs(r.value3());
        return product;
    }

ПользовательDTO:

    public static @Nullable UserDTO createOrNull(
        Record2<UserRecord, List<RoleDTO>> r
    ) {
        UserRecord rec = r.value1();
        if (rec.getUserId() == null) {
            return null;
        }
        UserDTO user = new UserDTO();
        user.from(r.value1())
        user.setRoles(r.value2());
        return user;
    }

Хорошо, что сами DTO также содержат методы «into»/«from», которые обеспечивают типобезопасность. Однако разработчику не следует забывать добавлять/удалять сюда новые/измененные поля, иначе они не будут отображаться. Например:

    @Override
    public void from(IUser from) {
        setUserId(from.getUserId());
        setClientId(from.getClientId());
        setEmail(from.getEmail());
        setFirstname(from.getFirstname());
        setLastname(from.getLastname());
    }

Поэтому сопоставление записи с DTO является типобезопасным и не вызывает проблем.

Другие вопросы по теме