TYPO3 DBAL Querybuilder: Nested SELECT statements? - typo3

Is it possible to build nested SELECT statements like the one below using the DBAL QueryBuilder?
SELECT i.id, i.stable_id, i.version, i.title
FROM initiatives AS i
INNER JOIN (
SELECT stable_id, MAX(version) AS max_version FROM initiatives GROUP BY stable_id
) AS tbl1
ON i.stable_id = tbl1.stable_id AND i.version = tbl1.max_version
ORDER BY i.stable_id ASC
The goal is to query an external non TYPO3 table which contains different versions of each data set. Only the data set with the highest version number should be rendered. The database looks like this:
id, stable_id, version, [rest of the data row]
stable_id is the external id of the data set. id is the internal autoincrement id. And version is also incremented automatically.
Code example:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
$result = $queryBuilder
->select(...$this->select)
->from($this->table)
->join(
'initiatives',
$queryBuilder
->select('stable_id, MAX(version) AS max_version' )
->from('initiatives')
->groupBy('stable_id'),
'tbl1',
$queryBuilder->and(
$queryBuilder->expr()->eq(
'initiatives.stable_id',
$queryBuilder->quoteIdentifier('tbl1.stable_id')
),
$queryBuilder->expr()->eq(
'initiatives.version',
$queryBuilder->quoteIdentifier('tbl1.max_version')
)
)
)
->orderBy('stable_id', 'DESC')
I cannot figure out the correct syntax for the ON ... AND statement. Any idea?

Extbase queries have JOIN capabilities but are otherwise very limited. You could use custom SQL (see ->statement() here), though.
A better API to build complex queries is the (Doctrine DBAL) QueryBuilder, including support for JOINs, database functions like MAX() and raw expressions (->addSelectLiteral()). Make sure to read until the ExpressionBuilder where it gets interesting.
So Extbase queries are useful in order to retrieve Extbase (model) objects. It can make implicit use of its knowledge of your data structure in order to save you some code but only supports rather simple queries.
The (Doctrine DBAL) QueryBuilder fulfills all other needs. If needed, you can convert the raw data to Extbase models, too. (for example $propertyMapper->convert($data, Job::class)).
I realize that we lack clear distinguishing between the two because they were both known at some time as "QueryBuilder", but they are totally different. That's why I like to add "Doctrine" when referring to the non-Extbase one.
An example with a JOIN ON multiple criteria.
$q = TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('fe_users');
$res = $q->select('*')
->from('tt_content', 'c')
->join(
'c',
'be_users',
'bu',
$q->expr()->andX(
$q->expr()->eq(
'c.cruser_id', $q->quoteIdentifier('bu.uid')
),
$q->expr()->comparison(
'2', '=', '2'
)
)
)
->setMaxResults(5)
->execute()
->fetchAllAssociative();

Short answer: it is not possible because the table to be joined in is generated on the fly. The related expression is back-ticked and thus causes an SQL error.
But: The SQL query can be changed to the following SQL query which does basically the same:
SELECT i1.id,stable_id, version, title, p.name, l.name, s.name
FROM initiatives i1
WHERE version = (
SELECT MAX(i2.version)
FROM initiatives i2
WHERE i1.stable_id = i2.stable_id
)
ORDER BY stable_id ASC
And this can be rebuild with the DBAL queryBuilder:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
$result = $queryBuilder
->select(...$this->select)
->from($this->table)
->where(
$queryBuilder->expr()->eq(
'initiatives.version',
'(SELECT MAX(i2.version) FROM initiatives i2 WHERE initiatives.stable_id = i2.stable_id)'
),
->orderBy('stable_id', 'DESC')
->setMaxResults( 50 )
->execute();

Related

Add exclusion to Doctrine Query Builder in JOIN

I have built the following query with the Doctrine Query Builder in my Symfony application.
$qb->select('c')
->from('AppBundle:Course', 'c')
->join('AppBundle:Log', 'a', Expr\Join::WITH, $qb->expr()->eq('c.id', 'a.course'))
->where($qb->expr()->in('a.type', ':type'))
->andWhere($qb->expr()->between('a.time', ':start', ':end'))
->andWhere($qb->expr()->eq('c.status', ':status'))
->setParameter(':type', ['opened'])
->setParameter(':standardScratchScore', [74])
->setParameter(':status', Course::OPENED)
->setParameter(':start', $dateFrom->format('Y-m-d H:i:s'))
->setParameter(':end', $dateTo->format('Y-m-d H:i:s'))
;
In my code I iterate over the Courses and then again query the Log table to check that an entry with a specific type doesn't exist for the Course. Is there a way I can incorporate the exclusion of log.type = 'log.sent-email' for this Course into this initial query, without using something like a sub-select?
Querying the same table again within the loop feels sub-optimal to me and NewRelic suggests it is hurting the performance of my application.
Well you can always join the table one more time for this specific need:
$qb->select('c')
->from('AppBundle:Course', 'c')
->join('AppBundle:Log', 'a', Expr\Join::WITH, $qb->expr()->eq('c.id', 'a.course'))
->leftjoin(
'AppBundle:Log',
'b',
Expr\Join::WITH,
$qb->expr()->andx(
$qb->expr()->eq('c.id', 'b.course'),
$qb->expr()->eq('b.type', 'log.sent-email')
))
) // join log a second time, with the type condition
->where($qb->expr()->in('a.type', ':type'))
->andWhere($qb->expr()->between('a.time', ':start', ':end'))
->andWhere($qb->expr()->eq('c.status', ':status'))
->andWhere($qb->expr()->isNull('b.type')) // Only select records where no log record is found
->setParameter(':type', ['opened'])
->setParameter(':standardScratchScore', [74])
->setParameter(':status', Course::OPENED)
->setParameter(':start', $dateFrom->format('Y-m-d H:i:s'))
->setParameter(':end', $dateTo->format('Y-m-d H:i:s'))
;

querydsl exists duplicate condition

here is my full sql as
new BooleanBuilder(
qClaimDataEntity.companyId.eq(companyId)
.or(
qClaimDataEntity.projectDataEntity.remoteProjectDataEntityList
.any().assignedCompanyId.eq(companyId)
)
.or(
qClaimDataEntity.projectDataEntity.remoteProjectDataEntityList
.any().remoteProjectStatus.eq(RemoteProjectStatusEnum.合作中)
)
);
it print sql string as
select
claimdatae0_.*
from
claim_data claimdatae0_
where
claimdatae0_.company_id=?
or exists (
select
1
from
project_data projectdat1_ cross
join
remote_project_data remoteproj2_
where
claimdatae0_.project_data_id=projectdat1_.project_data_id
and projectdat1_.project_data_id=remoteproj2_.project_data_id
and remoteproj2_.assigned_company_id=?
)
or exists (
select
1
from
project_data projectdat3_ cross
join
remote_project_data remoteproj4_
where
claimdatae0_.project_data_id=projectdat3_.project_data_id
and projectdat3_.project_data_id=remoteproj4_.project_data_id
and remoteproj4_.remote_project_status=?
) limit ?
but I think it can be done as
select
claimdatae0_.*
from
claim_data claimdatae0_
where
claimdatae0_.company_id=?
or exists (
select
1
from
project_data projectdat1_ cross
join
remote_project_data remoteproj2_
where
claimdatae0_.project_data_id=projectdat1_.project_data_id
and projectdat1_.project_data_id=remoteproj2_.project_data_id
and remoteproj2_.assigned_company_id=?
and remoteproj2_.remote_project_status=?
) limit ?
SubQuery is not support in latest Spring data jpa
and querydsl .any() don't support duplicate condition
is there another way to get the second query using querydsl?
The example is two separate or statements. I think the or should be nested, like this:
new BooleanBuilder(
qClaimDataEntity.companyId.eq(companyId)
.or(
qClaimDataEntity.projectDataEntity.remoteProjectDataEntityList
.any().assignedCompanyId.eq(companyId)
.or(
qClaimDataEntity.projectDataEntity.remoteProjectDataEntityList
.any().remoteProjectStatus.eq(RemoteProjectStatusEnum.合作中)
)
));

understanding complex SP in DB2

I need to make changes to an SP which has a bunch of complex XML functions and what not
Declare ResultCsr2 Cursor For
WITH
MDI_BOM_COMP(PROD_ID,SITE_ID, xml ) AS (
SELECT TC401F.T41PID,TC401F.T41SID,
XMLSERIALIZE(
XMLAGG(
XMLELEMENT( NAME "MDI_BOM_COMP",
XMLFOREST(
trim(TC401F.T41CTY) AS COMPONENT_TYPE,
TC401F.T41LNO AS COMP_NUM,
trim(TC401F.T41CTO) AS CTRY_OF_ORIGIN,
trim(TC401F.T41DSC) AS DESCRIPTION,
TC401F.T41EFR AS EFFECTIVE_FROM,
TC401F.T41EFT AS EFFECTIVE_TO,
trim(TC401F.T41MID) AS MANUFACTURER_ID,
trim(TC401F.T41MOC) AS MANUFACTURER_ORG_CODE,
trim(TC401F.T41CNO) AS PROD_ID,
trim(TC401F.T41POC) AS PROD_ORG_CODE,
TC401F.T41QPR AS QTY_PER,
trim(TC401F.T41SBI) AS SUB_BOM_ID,
trim(TC401F.T41SBO) AS SUB_BOM_ORG_CODE, --ADB01
trim(TC401F.T41VID) AS SUPPLIER_ID,
trim(TC401F.T41SOC) AS SUPPLIER_ORG_CODE,
TC401F.T41UCT AS UNIT_COST
)
)
) AS CLOB(1M)
)
FROM TC401F TC401F
GROUP BY T41PID,T41SID
)
SELECT
RowNum, '<BOM_INBOUND>' ||
XMLSERIALIZE (
XMLELEMENT(NAME "INTEGRATION_MESSAGE_CONTROL",
XMLFOREST(
'FULL_UPDATE' as ACTION,
'POLARIS' as COMPANY_CODE,
TRIM(TC400F.T40OCD) as ORG_CODE,
'5' as PRIORITY,
'INBOUND_ENTITY_INTEGRATION' as MESSAGE_TYPE,
'POLARIS_INTEGRATION' as USERID,
'TA' as RECEIVER,
HEX(Generate_Unique()) as SOURCE_SYSTEM_TOKEN
),
XMLELEMENT(NAME "BUS_KEY",
XMLFOREST(
TRIM(TC400F.T40BID) as BOM_ID,
TRIM(TC400F.T40OCD) as ORG_CODE
)
)
) AS VARCHAR(1000)
)
|| '<MDI_BOM>' ||
XMLSERIALIZE (
XMLFOREST(
TRIM(TC400F.T40ATP) AS ASSEMBLY_TYPE,
TRIM(TC400F.T40BID) AS BOM_ID,
TRIM(TC400F.T40CCD) AS CURRENCY_CODE,
TC400F.T40DPC AS DIRECT_PROCESSING_COST,
TC400F.T40EFD AS EFFECTIVE_FROM,
TC400F.T40EFT AS EFFECTIVE_TO,
TRIM(TC400F.T40MID) AS MANUFACTURER_ID,
TRIM(TC400F.T40MOC) AS MANUFACTURER_ORG_CODE,
TRIM(TC400F.T40OCD) AS ORG_CODE,
TRIM(TC400F.T40PRF) AS PROD_FAMILY,
TRIM(TC400F.T40PID) AS PROD_ID,
TRIM(TC400F.T40POC) AS PROD_ORG_CODE,
TRIM(TC400F.T40ISA) AS IS_ACTIVE,
TRIM(TC400F.T40VID) AS SUPPLIER_ID,
TRIM(TC400F.T40SOC) AS SUPPLIER_ORG_CODE,
TRIM(TC400F.T40PSF) AS PROD_SUB_FAMILY,
CASE TRIM(TC400F.T40PML)
WHEN '' THEN TRIM(TC400F.T40PML)
ELSE TRIM(TC400F.T40PML) || '~' || TRIM(TC403F.T43MDD)
END AS PROD_MODEL
) AS VARCHAR(3000)
)
|| IFNULL(MBC.xml, '') ||
XMLSERIALIZE (
XMLFOREST(
XMLFOREST(
TRIM(TC400F.T40CCD) AS CURRENCY_CODE,
TC400F.T40PRI AS PRICE,
TRIM(TC400F.T40PTY) AS PRICE_TYPE
) AS MDI_BOM_PRICE,
XMLFOREST(
TRIM(TC400F.T40CCD) AS CURRENCY_CODE,
TRIM(TC400F.T40PRI) AS PRICE,
'TRANSACTION_VALUE' AS PRICE_TYPE
) AS MDI_BOM_PRICE,
XMLFOREST(
TRIM(TC400F.T40INA) AS INCLUDE_IN_AVERAGING
) AS MDI_BOM_IMPL_BOM_PROD_FAMILY_AUTOMOBILES
) AS VARCHAR(3000)
)
|| '</MDI_BOM>' ||
'</BOM_INBOUND>' XML
FROM (
SELECT
ROW_NUMBER() OVER (
ORDER BY T40STS
,T40SID
,T40BID
) AS RowNum
,t.*
FROM TC400F t
) TC400F
LEFT OUTER JOIN MDI_BOM_COMP MBC
ON TC400F.T40SID = MBC.SITE_ID
AND TC400F.T40PID = MBC.PROD_ID
LEFT OUTER JOIN TC403F TC403F
ON TC400F.T40PML <> ''
AND TC400F.T40PML = TC403F.T43MDL
WHERE TC400F.T40STS = '10'
AND TC400F.RowNUM BETWEEN
(P_STARTROW + (P_PAGENOS - 1) * P_NBROFRCDS)
AND (P_STARTROW + (P_PAGENOS - 1) * P_NBROFRCDS +
P_NBROFRCDS - 1);
Given above is a cursor declaration in the SP code which I am struggling to understand. The very first WITH itself seems to be mysterious. I have used it along with temporary table names but this is the first time, Im seeing something of this sort which seems to be an SP or UDF? Can someone please guide me on how to understand and make sense out of all this?
Adding further to the question, the actual requirement here is to arrange the data in the XML such a way that that those records which have TC401F.T41SBI field populated should appear in the beginning of the XML output..
This field is being selected as below in the code:
trim(TC401F.T41SBI) AS SUB_BOM_ID. If this field is non-blank, this should appear first in the XML and any records with this field value Blank should appear only after. What would be the best approach to do this? Using ORDER BY in any way does not really seem to help as the XML is actually created through some functions and ordering by does not affect how the items are arranged within the XML. One approach I could think of was using a where clause where TC401F.T41SBI <> '' first then append those records where TC401F.T41SBI = ''
Best I can do is help with the CTE.
WITH
MDI_BOM_COMP(PROD_ID,SITE_ID, xml ) AS (
SELECT TC401F.T41PID,TC401F.T41SID,
This just generates a table named MDI_BOM_COMP with three columns named PROD_ID, SITE_ID, and XML. The table will have one record for each PROD_ID, SITE_ID, and the contents of XML will be an XML snippet with all the components for that product and site.
Now the XML part can be a bit confusing, but if we break it down into it's scalar and aggregate components, we can make it a bit more understandable.
First ignore the grouping. so the CTE retrieves each row in TC401F. XMLELEMENT and XMLFORREST are scalar functions. XMLELEMENT creates a single XML element The tag is the first parameter, and the content of the element is the second in the above example. XMLFORREST is like a bunch of XMLELEMENTs concatenated together.
XMLSERIALIZE(
XMLAGG(
XMLELEMENT( NAME "MDI_BOM_COMP",
XMLFOREST(
trim(TC401F.T41CTY) AS COMPONENT_TYPE,
TC401F.T41LNO AS COMP_NUM,
trim(TC401F.T41CTO) AS CTRY_OF_ORIGIN,
trim(TC401F.T41DSC) AS DESCRIPTION,
TC401F.T41EFR AS EFFECTIVE_FROM,
TC401F.T41EFT AS EFFECTIVE_TO,
trim(TC401F.T41MID) AS MANUFACTURER_ID,
trim(TC401F.T41MOC) AS MANUFACTURER_ORG_CODE,
trim(TC401F.T41CNO) AS PROD_ID,
trim(TC401F.T41POC) AS PROD_ORG_CODE,
TC401F.T41QPR AS QTY_PER,
trim(TC401F.T41SBI) AS SUB_BOM_ID,
trim(TC401F.T41SBO) AS SUB_BOM_ORG_CODE, --ADB01
trim(TC401F.T41VID) AS SUPPLIER_ID,
trim(TC401F.T41SOC) AS SUPPLIER_ORG_CODE,
TC401F.T41UCT AS UNIT_COST
)
)
) AS CLOB(1M)
So in the example, for each row in the table, XMLFORREST creates a list of XML elements, one for each of COMPONENT_TYPE, COMP_NUM, CTRY_OF_ORIGIN, etc. These elements form the content of another XML element MDI_BOM_COMP which is created by XMLELEMENT.
Now for each row in the table we have selected PROD_ID, SITE_ID, and created some XML. Next we group by PROD_ID, and SITE_ID. The aggregation function XMLAGG will collect all the XML for each PROD_ID and SITE_ID, and concatenate it together.
Finally XMLSERIALIZE will convert the internal XML representation to the string format we all know and love ;)
I think I found the answer for my requirement. I had to add an order by field name after XMLELEMENT function

Need help in creating CriteriaQuery

First of all, I would like to know if it is possible to do?
Below is my query and I am trying to build using criteria.
SELECT CONCAT('record-', rl.record_id) AS tempId,
'sloka' AS type,
rl.record_id AS recordId,
rl.title AS title,
rl.locale as locale,
rl.intro AS intro,
rl.title AS localetitle,
NULL AS audioUrl,
lp.name AS byName,
lp.person_id AS byId,
lp.name AS onName,
lp.person_id AS onId
FROM record_locale rl
LEFT JOIN record r ON rl.record_id = r.record_id
LEFT JOIN locale_person lp ON r.written_on = lp.person_id
WHERE rl.title LIKE :title
AND rl.locale = :locale
AND lp.locale = :locale
UNION
SELECT CONCAT('lyric-', s.song_id) AS tempId,
'bhajan' AS type,
s.song_id AS recordId,
s.title,
l.locale as locale,
NULL AS intro,
l.title AS localetitle,
s.audio_url AS audioUrl,
lpb.name AS byName,
lpb.person_id AS byId,
lpo.name AS onName,
lpo.person_id AS onId
FROM song s
LEFT JOIN locale_person lpb
ON (s.written_by = lpb.person_id AND lpb.locale = :locale)
LEFT JOIN locale_person lpo
ON (s.written_on = lpo.person_id AND lpo.locale = lpb.locale)
INNER JOIN lyric l
ON (l.locale = lpb.locale AND l.song_id = s.song_id)
WHERE s.title LIKE :title AND s.approved_by IS NOT NULL
ORDER BY localeTitle ASC
// END
Based on few conditions, I might need to have union of both queries or just individual query without union.
Converting the SQL to JPQL is usually a good first step, as we can't quite tell what these tables map to, or what you are expecting to get back. If it is possible to do in JPQL, it should be possible with a criteria query. Except in this case: JPA/JPQL does not have the union operator so it won't work in straight JPA, but some providers such as EclipseLink have support. See:
UNION to JPA Query
and
http://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/j_union.htm

Laravel 5, Derived table in join clause?

I have this query:
SELECT * FROM blog
LEFT JOIN (
SELECT blog_id, AVG(value) as blog_rating FROM blog_ratings
GROUP BY (blog_id)
) T ON T.blog_id = blog.id;
I do not know how to write this with Eloquent.
For Example:
Blog::select("*")->leftJoin( /* Here goes derived table */ )->get()
How do I accomplish this?
I'd personally just use the fluent query builder, try this out and see how it works out:
DB::table('blog')
->select('*')
->leftJoin(DB::raw('(SELECT blog_id, AVG(value) as blog_rating FROM blog_ratings
GROUP BY (blog_id)
) as T'), function ($join) {
$join->on ( 'T.blog_id', '=', 'blog.id' );
})
->get();
You can always swap ->get() for ->toSql() to dump out the query and adjust if you see any mistakes.