У меня есть следующие DTO:
ПродуктDTO
ПользовательDTO
ПользовательРолеДТО:
Мне нужно получить список продуктов, и каждый из этих продуктов имеет поле «creatorId» (NULLABLE).
Продукт -> 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, поэтому я могу использовать операторы соединения для дополнительной фильтрации запроса.
Привет, Лукас, спасибо за твой совет, я опубликовал ответ, и теперь я заставил его работать с помощью специальных новообращенных. Мой ответ не выглядит очень элегантно, но он делает то, что должен (выполняйте преобразование до выполнения оператора, чтобы преобразование могло оставаться в абстракции). Так что в моем ответе это как бы неявно. Если вы знаете еще более элегантный способ, возможно, вы сможете ответить там :)
Еще раз спасибо за последовательные ответы! очень ценю, и я многому научился в процессе :)
С помощью Лукаса Эдера (см. выше) и связанной страницы с примерами для специальных преобразователей.
Таким образом, этот код можно повторно использовать для разных вариантов использования (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
и отражение. Это можно сделать полностью типобезопасным способом.
Что касается подсчета, то зачем это нужно? Пагинация? Рассматривали ли вы возможность использования оконной функции для вычисления этого значения счетчика непосредственно с самим запросом, чтобы избежать дополнительных обращений туда и обратно? Пример смотрите в этой статье
1) я использую asterisk()
, потому что DTO со временем часто меняются… мы работаем в SCRUM-спринтах и очень часто добавляем/удаляем функции. Код часто меняется. С помощью asterisk() все поля в DTO сопоставляются автоматически. В противном случае разработчикам придется изменить все места, где поля использовались при выборе, когда поле добавляется/изменяется/удаляется посредством миграции схемы базы данных. Если в этом подходе есть что-то изначально неправильное, я был бы рад узнать об этом, но asterisk() мне показался довольно удобным, и, конечно, часто не все поля необходимы постоянно, но в большинстве наших случаев они необходимы.
в моем примере я попробовал применить преобразование на каждом вложенном уровне. Но когда я применил преобразование к as("roles")
, я получил ошибку во время выполнения SQL, которую я не смог устранить. Кажется, это работало только тогда, когда я применял преобразование только к мультимножеству «создатель», а не к мультимножеству «роли».
Я прочитаю об оконной функции для (возможно) подсчета в одном операторе и избежания дополнительного обхода, спасибо. Не знал, что это возможно.
2) Я использовал Pojo, сгенерированные Jooq, в качестве источника для своих DTO, но удалил конструкторы... возможно, мне также следует скопировать их, когда они могут помочь с безопасностью: D
Что ж, что по своей сути «неправильно», так это 1) то, что вы в конечном итоге будете проецировать слишком много данных, и это будет очень сложно исправить позже в ваших циклах разработки, так что здесь, по крайней мере, ждет проблема с производительностью. во многих случаях (хотя вы сказали, что не в вашем). 2) asterisk будет генерировать порядок столбцов, указанный в (производственной) базе данных, в отличие от того, что генератор кода jOOQ мог ожидать во время компиляции, поэтому с этим могут быть связаны редкие, тонкие ошибки. Возможно, лучший проект Table.fields()
или просто Table
напрямую.
Что касается исключения во время выполнения, то действительно гораздо лучше, если вы сможете просто показать простой воспроизводитель, а не описывать его словами...
спасибо, Лукас, я задам новый вопрос об ошибке, а также заменю звездочку и попытаюсь вместо этого использовать конструкторы для безопасного отображения типов.
Давайте продолжим обсуждение в чате.
Некоторые общие практические правила:
asterisk()
..class
в ваших картографах (которые используют отражение, что не является типобезопасным). Конечно, вы все еще можете это сделать, в принципе это поддерживается, но тогда вы не будете знать, правильно ли вы написали запрос и сопоставление, пока не запустите его.Не правда ли, это выглядит намного проще?
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
я упростил пример настолько, насколько мог, чтобы по-прежнему выдавать ошибку. Ниже приведен сокращенный пример, который все еще выдает ошибку во время выполнения: dsl().select(PRODUCT.PRODUCTID, row(PRODUCT.fk_product_creatorId().USERID)) .from(Product.PRODUCT);
@funkrusher: Кажется, это не имеет отношения к делу, и с этим вам сложно помочь в разделе комментариев здесь. Не могли бы вы задать новый вопрос со всеми необходимыми подробностями?
Привет, Лукас! Что касается упомянутых здесь «практических правил», можете ли вы сказать мне, можно ли им следовать/применять только в том случае, если таблица в запросе содержит максимум 22 столбца? Мне кажется, что как только таблица содержит больше столбцов, чем 22, типобезопасный подход больше не применим, и тогда можно/должно выбрать другой подход (или провести рефакторинг таблицы)
@funkrusher: Это практические правила, поэтому всегда есть исключения. Скорее всего, при больших прогнозах вы будете объединять атрибуты, которые принадлежат друг другу, так что вы все равно сможете оставаться ниже предела в 22 с точки зрения jOOQ. Но в конечном итоге многие дороги ведут в Рим. Делайте то, что лучше всего подходит для вас.
Хотя принятый ответ — лучший способ решить эту проблему с помощью чистой настройки, посмотрев на некоторые таблицы в моем проекте, которые имеют более 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 является типобезопасным и не вызывает проблем.
Почему бы не использовать рекомендуемый подход с использованием специальных конвертеров?