DBIx::Class Update one table from another - mysql

I have a SQL query which works fine directly in MySQL, but I'm struggling to convert it to use via DBIx::Class, I've simplified the query here
UPDATE table1, table2
SET table1.field1 = SOMEFUNC( table1.field4 / table2.field2 )
WHERE table1.id = table2.id
AND table1.field3 = table2.field3
AND table2.field2 IS NOT NULL
AND table2.field2 > 0;
Any suggestions?

In similar cases I usually perform a search on the related table. In your case you have two result sources: table1 and table2. table1 has a relationship to the table table2 (most probably) named table2. So now you perform a search in the table2 using this relationship table2.
my $resultset = $schema->resultset('table1')->search_rs(
{
me.field3 => table2.field3,
table2.field2 => {'!=', '' },
table2.field2 > 0
},
{
'join' => 'table2',
'select' => ['me.field1', 'me.field4', 'table2.field2', 'me.field4' / 'table2.field2' ],
'as' => ['field1','field4', 'field2', 'division_result']
}
);
while ( my $this_res_row = $resultset->next ) {
## Presuming somefunc is a perl subroutine
$this_res_row->update( { field1 => somefunc( $this_res_row->get_column('division_result') ) } );
}
Maybe this is the solution you have come up with, I think it is not possible to do what you want in a single dbic update() statement. Unfortunately, each update() statement will hit your database separately.
By the way, I don't think it is a good idea ta make a second union on the same tables via field3, you have it already done via id fields. Why would you need another one?
Alternatively, and I would consider it a better idea, is to use database triggers to implement your code.
Please, take into account, that I haven't tested the code and it may contain some mistakes, but you have to grasp the idea of how to get what you want.

What about using *_related methods?

Related

Execute WITH MySQL expression in a query using Laravel db builder

Can't find any info on how to execute something like
WITH table AS (
SELECT colA, colB
FROM table2 INNER JOIN table1 ON table1.id = table2.colA
),
table4 AS (
SELECT moo, foo
INNER JOIN table3 ON table3.colC = table4.colD
),
......
using Laravel db query builder and the expression WITH
Does anybody have build such query and have clue how to be executed?
It's perfectly possible, I use it a lot.
For example, I have a $query and I have an array called $params (the prepared statements).
Than I do:
$connection = DB::connection('mysql');
$connection->getPdo()->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
$result = $connection->select($query, $params);
I need the PDO::ATTR_EMULATE_PREPARES since I have repeating params (e.g. multiple times :user_id in the query).
So basically, I use a raw query. It is possible to also use such a query on an eloquent model, in which case it will return models as you are used to in Laravel. But this example really shows the basic version.

Query matching() causing duplicate rows and distinct() not working

I am trying to filter one table Payments by a field on the associated table Invoices.
Using the function matching() on the query object filters correctly but causes duplicate rows. It seemed like the solution was using distinct(), but calling distinct(Payments.id) results in an invalid query. I'm doing the following in a controller action.
$conditions = [
'Payments.is_deleted =' => false
];
$args = [
'conditions' => $conditions,
'contain' => ['Invoices', 'Invoices.Clients'],
];
$payments = $this->Payments->find('all', $args);
if($issuer) {
// This causes duplicate rows
$payments->matching('Invoices', function ($q) use ($issuer) {
return $q->where(['Invoices.issuer_id' => $issuer['id']]);
});
// $payments->distinct('Payments.id'); // Causes a mysql error
}
Am I correct in thinking that distinct() is what I need, and if so any idea what's missing to make it work?
I'm getting the following mysql error when uncommenting the line above:
Error: SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #8 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'InvoicesPayments.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
Full query:
SELECT
PAYMENTS.ID AS `PAYMENTS__ID`,
PAYMENTS.CREATED AS `PAYMENTS__CREATED`,
PAYMENTS.MODIFIED AS `PAYMENTS__MODIFIED`,
PAYMENTS.DATE_REGISTERED AS `PAYMENTS__DATE_REGISTERED`,
PAYMENTS.USER_ID AS `PAYMENTS__USER_ID`,
PAYMENTS.AMOUNT AS `PAYMENTS__AMOUNT`,
PAYMENTS.IS_DELETED AS `PAYMENTS__IS_DELETED`,
INVOICESPAYMENTS.ID AS `INVOICESPAYMENTS__ID`,
INVOICESPAYMENTS.INVOICE_ID AS `INVOICESPAYMENTS__INVOICE_ID`,
INVOICESPAYMENTS.PAYMENT_ID AS `INVOICESPAYMENTS__PAYMENT_ID`,
INVOICESPAYMENTS.PART_AMOUNT AS `INVOICESPAYMENTS__PART_AMOUNT`,
INVOICES.ID AS `INVOICES__ID`,
INVOICES.CREATED AS `INVOICES__CREATED`,
INVOICES.MODIFIED AS `INVOICES__MODIFIED`,
INVOICES.IS_PAID AS `INVOICES__IS_PAID`,
INVOICES.IS_DELETED AS `INVOICES__IS_DELETED`,
INVOICES.CLIENT_ID AS `INVOICES__CLIENT_ID`,
INVOICES.ISSUER_ID AS `INVOICES__ISSUER_ID`,
INVOICES.NUMBER AS `INVOICES__NUMBER`,
INVOICES.SUBTOTAL AS `INVOICES__SUBTOTAL`,
INVOICES.TOTAL AS `INVOICES__TOTAL`,
INVOICES.DATE_REGISTERED AS `INVOICES__DATE_REGISTERED`,
INVOICES.CURRENCY AS `INVOICES__CURRENCY`,
INVOICES.RECEIVER_NAME AS `INVOICES__RECEIVER_NAME`,
INVOICES.RECEIVER_RFC AS `INVOICES__RECEIVER_RFC`,
INVOICES.EMAIL_SENDER AS `INVOICES__EMAIL_SENDER`,
INVOICES.PDF_PATH AS `INVOICES__PDF_PATH`
FROM
PAYMENTS PAYMENTS
INNER JOIN
INVOICES_PAYMENTS INVOICESPAYMENTS
ON PAYMENTS.ID = (
INVOICESPAYMENTS.PAYMENT_ID
)
INNER JOIN
INVOICES INVOICES
ON (
INVOICES.ISSUER_ID = :C0
AND INVOICES.ID = (
INVOICESPAYMENTS.INVOICE_ID
)
)
WHERE
(
PAYMENTS.IS_DELETED = :C1
AND PAYMENTS.DATE_REGISTERED >= :C2
AND PAYMENTS.DATE_REGISTERED <= :C3
)
GROUP BY
PAYMENT_ID
ORDER BY
PAYMENTS.DATE_REGISTERED ASC
That behavior is expected, as matching will use an INNER join, and yes, grouping is how you avoid duplicates:
As this function will create an INNER JOIN, you might want to consider calling distinct on the find query as you might get duplicate rows if your conditions don’t exclude them already. This might be the case, for example, when the same users comments more than once on a single article.
Cookbook > Database Access & ORM > Query Builder > Loading Associations > Filtering by Associated Data
As the error message states, your MySQL server is configured to use the strict only_full_group_by mode, where your query is invalid. You can either disable that strict mode as mentioned by Akash prajapati (which can come with its own problems, as MySQL is then allowed to pretty much pick values of a group at random), or you could change how you query things in order to conform to the strict mode.
In your case where you need to group on the primary key, you could simply switch to using innerJoinWith() instead, unlike matching() this will not add any fields of that association to the SELECT list, and things should be fine in strict mode, as everything else is functionally dependent:
In cases where you would group on a key that would break functional dependency detection, one way to solve that could for example be to use a subquery for filtering, one that only selects that key, something along the lines of this:
$conditions = [
'Payments.is_deleted =' => false
];
$payments = $this->Payments
->find()
->contain(['Invoices.Clients']);
if($issuer) {
$matcherQuery = $this->Payments
->find()
->select(['Payments.some_other_field'])
->where($conditions)
->matching('Invoices', function ($q) use ($issuer) {
return $q->where(['Invoices.issuer_id' => $issuer['id']]);
})
->distinct('Payments.some_other_field');
$payments->where([
'Payments.some_other_field IN' => $matcherQuery
]);
} else {
$payments->where($conditions);
}
This will result in a query similar to this, where the outer query can then select all the fields you want:
SELECT
...
FROM
payments
WHERE
payments.some_other_field IN (
SELECT
payments.some_other_field
FROM
payments
INNER JOIN
invoices_payments ON
payments.id = invoices_payments.payment_id
INNER JOIN
invoices ON
invoices.issuer_id = ...
AND
invoices.id = invoices_payments.invoice_id
WHERE
payments.is_deleted = ...
GROUP BY
payments.some_other_field
)
The problem with sql_mode value in mysql so you need to set the sql_mode value as blank and then you can try and working fine for you.
SET GLOBAL sql_mode=(SELECT REPLACE(##sql_mode,'ONLY_FULL_GROUP_BY',''));
Please let me know still anything else.
I had the same issue, but was too afraid to set the sql_mode as mentioned by #Akash and also too much in a hurry to restructure the query. So I decided to use the inherited Collection method indexBy()
https://book.cakephp.org/4/en/core-libraries/collections.html#Cake\Collection\Collection::indexBy
$resultSetFromYourPaymentsQuery = $resultSetFromYourPaymentsQuery->indexBy('id');
It worked like a charm and it is DB independent.
EDIT: After some more tinkering, this might not be practical for all use cases. Replacing matching with innerJoinWith as proposed in the accepted answer will probably solve it in more generalized manner.

How could I create a sub-query in cakePHP?

How could I create a sub-query in cakePHP with find method? For example:
SELECT *, (SELECT COUNT(*) FROM table2 WHERE table2.field1 = table1.id) AS count
FROM table1
WHERE table1.field1 = 'value'
!!! table2.field1 = table1.id !!!
Just for addition, you can build subquery using Cake's ORM. It will be more easy. Please read CakePHP retrieving data doc
In general you can use buildStatement() method of DataSource object. You can save all logic, including pagination etc.
You can do this one of two ways:
1. Use $this->Model->query(...)
This allows you execute SQL directly using the query you posted above but be aware that it can make your application quite brittle if the database schema were to change. Probably the fastest.(Documentation)
Example
$this->Model1->query("SELECT * FROM model;");
2. Separate the Calls
This is probably not as fast as option 1 but it does give you the ability to break your query down into a number of steps. You're also unlikely to get SQL injection which is potential risk with option 1.
Example
$model1s = $this->Model1->find
(
'all',
array
(
'conditions' => array('Model1.field1' => 'value')
)
);
foreach($model1s as $model1)
{
$model1['model2_count'] = $this->Model2->find
(
'count',
array
(
'conditions' => array
(
'Model2.field1' => $model1['id']
)
)
);
}

MySQL return fields and data from a join with table name?

Is it possible in mysql to return the tablename.field in a join without manually setting the 'AS' ? For instance, let's pretend I'd like to run this query :
SELECT * FROM table1 LEFT JOIN table2 ON table1.id = table2.id;
What i'd like to obtain is something like
table1.prop1 = 'some value'
table1.prop2 = 'some value'
table2.prop1 = 'some value'
...
Possible ??
Nope, not according to the SQL spec. You can, however, build a udf or other higher language function to do this for you...

same Linq for two tables

I need to do something like this,
My two tables have the same signature, but different class so It suppose to work but it is not working.
var myTable;
if (booleanVariable == true)
{
myTable = table1;
}
else
{
myTable = table2;
}
var myLinq1 = from p in myTable
join r in myOtherTable
select p;
In this case, I have to initialize myTable
I have tried also,
var myTable= table2;
if (booleanVariable == true)
{
myTable = table1;
}
var myLinq1 = from p in myTable
join r in myOtherTable
select p;
then var is type table2, then it can't be changed to table1 type.
I need help, I don't want to make a copy paste of all the code. the linq query is huge, and it s nested with 5 or 6 queries. also I have to do this on 12 different methods.
Thanks a lot for your help.
Don't know if this will work, never tried it, but...
If both classes can implement the same interface or share a base class, could you maybe do:
var q = (booleanVariable)
? from p in myTable1 select (ISomeInterface)p
: from p in myTable2 select (ISomeInterface)p;
If you use this often, you can put this into its own method - I believe it will return IQueryable<ISomeInterface>.
Then connect to the rest of the LINQ query using the LINQ methods instead of the LINQ C# syntax (i.e. OrderBy() instead of orderby). I just don't know if LINQ-to-SQL will be smart enough to translate this into the correct SQL query.
But I agree with Jon's comment - this is probably bad design, and should be revisited. If you can't reorganize the tables themselves, and if you only need read access, how about a view that unions the two tables together, then tie that view into your LINQ-to-SQL structure.