Laravel decrement column value but not negative - php

I know i can reduce the column value in laravel using this query
DB::table('users')->decrement('votes', 5);
But i want to restrict the value from being become negative value.
Is there anyway to do this with laravel?

You'll need to use raw queries for that.
The following code should do the trick for all DB engines that support GREATEST function:
DB::table('users')->update(['votes' => DB::raw('GREATEST(votes - 5, 0)')]);
It will decrement votes column in users table by 5, but won't go below zero - that's what GREATEST function is used for.

If you really want to use decrement in this case (could be handy if you're accessing it through a relationship for example), you could do something like:
$thing->decrement('votes', $thing->votes - $amount <= 0 ? $thing->votes : $amount);
Where $amount is 5 in your case. It's pretty ugly in my opinion. Worth noting if you already have $thing (say via a relationship) it won't trigger an additional query when accessing votes.
If you are only incrementing by 1, a simply if wrapped around is cleaner:
if($thing->votes > 0) {
$thing->decrement('votes');
}

Related

PHP increment booking number according to the last booking number in database

I'm using PHP 7 with Phalcon PHP and I'm trying to create a method to generate a booking number. Here is my current method :
public function generateNumber($company_code) {
// Build the prefix : COMPANY20190820
$prefix = $company_code . date('Ymd');
// It's like SELECT count(*) FROM bookings WHERE number LIKE 'COMPANY20190820%'
$counter = Bookings::count(array(
"number LIKE :number:",
"bind" => array('number' => $prefix.'%')
));
// Concat prefix with bookings counter with str_pad
// COMPANY20190820 + 005 (if 4 bookings in DB)
$booking_number = $prefix . str_pad($counter + 1, 3, 0, STR_PAD_LEFT);
// Return COMPANY20190820005
return $booking_number;
}
So I have a problem because sometime I have to delete 1 or multiple bookings so I can get :
COMPANY20190820001
COMPANY20190820002
COMPANY20190820005
COMPANY20190820006
COMPANY20190820007
And I need to add after the last in my DB so here 007, because I can get duplicated booking number if I count like that.
So how can I do to take the last and increment according the last booking number of the current day ?
You need to rethink what you want to do here as it will never work that way.
As I see it you have at least two options:
Use an auto-increment id and use that in combination with the prefix
Use a random fairly unique string (e.g. UUID4)
You should never manually try to get the current maximum id as that may and most likely will at some point result in race conditions and brittle code as a result of that.
So I found a solution, maybe there is a better way to do that but my function works now:
public function generateNumber($company_code) {
// Build the prefix : COMPANY20190820
$prefix = $company_code . date('Ymd');
// Get the last booking with the today prefix
// e.g : COMPANY20190820005
$last_booking = Bookings::maximum(array(
"column" => "number",
"number LIKE :number:",
"bind" => array('number' => $prefix.'%')
));
// Get the last number by removing the prefix (e.g 005)
$last_number = str_replace($prefix, "", $last_booking);
// trim left 0 if exist to get only the current number
// cast to in to increment my counter (e.g 5 + 1 = 6)
$counter = intval(ltrim($last_number, "0")) + 1;
// Concat prefix + counter with pad 006
$booking_number = $prefix . str_pad($counter, 3, 0, STR_PAD_LEFT);
// Return COMPANY20190820006
return $booking_number;
}
I reckon that the use case you describe does not justify the hassle of writing a custom sequence generator in PHP. Additionally, in a scenario where booking deletion is expected to happen, ID reusing feels more a bug than a feature, so your system should store a permanent counter to avoid reusing, making it less simple. Don't take me wrong, it can be done and it isn't rocket science, but it's time and energy you don't need to spend.
Your database engine surely has a native tool to generate autoincremented primary keys, with varying names and implementations (SQL Server has identity, Oracle has sequences and identity, MySQL has auto_increment...). Use that instead.
Keep internal data and user display separated. More specifically, don't use the latter to regenerate the former. Your COMPANY20190820007 example is trivial to compose from individual fields, either in PHP:
$booking_number = sprintf('%s%s%03d',
$company_code,
$booking_date->format('Ymd'),
$booking_id
);
... or in SQL:
-- This is MySQL dialect, other engines use their own variations
SELECT CONCAT(company_code, DATE_FORMAT(booking_date, '%Y%m%d'), LPAD(booking_id, 3, '0')) AS booking_number
FROM ...
You can (and probably should) save the resulting booking_number, but you cannot use it as source for further calculations. It's exactly the same case as dates: don't need to store dates in plain English in order to eventually display them to the end-user and you definitively don't want to parse English dates back to actual dates in order to do anything else beyond printing.
You also mention the possibility of generating long pure-digit identifiers, as Bookings.com does. There're many ways to do it and we can't know which one they use, but you may want to considering generating a numeric hash out of your auto-incremented PK via integer obfuscation.
you could split your database field in two parts, so you hold the prefix and the counter separately.
then, you simply select the highest counter for your desired prefix and increment that one.
if you can't change the table structure, you could alternatively order by the id descendingly and select the first. then you can extract its counter manually. keep in mind you should pad the numbers then, or you get #9 even if #10 exists.
if padding is not an option, you can direct the database to replace your prefix. that way, you can cast the remaining string to a number and let the database sort - this will cost some performance, though, so keep the amount of records low.

Laravel from database table to another table 1mill rows, slow

So basically, I got 2 tables.
The first table contains 1 million rows or something, with an empty field called 'telefon'.
Now, I got a second table, which has the field values for 'telefon' in the other table.
I came up with this solution, but this takes forever. It has been an hour, and when inspecting the database table, only 1600 rows are done. Is there any faster ways of doing this? Thanks in advance.
DB::table('phones') -> orderBy('id') -> chunk(100, function($old) {
foreach ($old as $x) {
DB::table('companies')
-> where('organisasjonsnummer', $x -> businessCode)
-> update([
'telefon' => $x -> contact
]);
}
});
Huh, foreach + queries is almost always bad. If I am not mistaken, you would like to do this:
UPDATE companies, phones SET companies.telefon = phones.contact WHERE companies.organisasjonsnummer = phones.businessCode
It may be very slow if there's no index on companies.organisasjonsnummer and phones.businessCode columns, but it can take a lot of time to index them now as well, so I'm not sure if there's any benefit to index them now if they won't be used later. Anyway, using a single query should be faster at least to some extent.
Always remember, when you use Eloquent/SQL inside a loop, you will run a command for each round.
The same applies for lazy loading.
In this case you should use \DB::statement("put your sql here"); or in this case \DB::update("put your update here");, let the database do the service for you and be happy!.

Compare if value is within range of number mysql

In my database i have 2 columns namely: Min and Max they are varchar and they have number values which i need to compare with. My query is something like this
SELECT * FROM Pricing_tbl WHERE Productid='10'
and i have this to compare the price
while ($selected_row = $stmt - > fetch(PDO::FETCH_ASSOC)) {
if ($marketval > $selected_row['Min'] && $marketval < $selected_row['Max']) {
$price[] = array('price_level' => $selected_row['price_level']);
}
}
This is ok if values of Min and Max are always numbers. But there is an instance where the value of Max is above. How to compare this situation?
Example values will be
Or should i just change the Max value. Any suggestion is appreciated
Assuming a maximum of above means all the way to infinity and you want to keep the data as is 1, you can just change the condition:
$marketval > $selected_row['Min'] && $marketval < $selected_row['Max']
into something like:
$marketval >= $selected_row['Min'] &&
($selected_row['Max'] == 'above' || $marketval < $selected_row['Max'])
In other words a value is considered under the maximum always if the maximum is the word above. Otherwise the actual (original) comparison decides.
PHP short-circuiting on logical operators will ensure that the second half of the or section will never be evaluated if the first half is true.
Note the change I made to the minimum comparison as well, the use of >= rather than >. As it was, a value like 300000.01 would not have been caught.
You may also want to coerce the numerics in the comparisons with $marketValue. If both $marketValue and $selectedRow['whatever'] are strings, I think they'll still use numeric comparison but I usually try to be explicit so I don't have to think about it :-)
1 There are probably better ways to do this, other than storing what's mostly numeric data as strings just because you want to be able to store the value 'above'.
Both methods below involve converting the column type to a numeric one which will allow better comparisons, including having the DBMS itself work it out rather than having to get all data and post-process it with PHP. Data manipulation is what a DBMS does best so it's generally better to leave that manipulation up to the DBMS for efficiency.
The first is to put a ridiculously large number in the maximum for the 'above' row so that your simple 'in between min and max' check will work. Using the DBMS itself to get the pricing level would be a simple:
select pricing_level
from pricing_tbl
where product_id = '10'
and $market_val >= minval
and $market_val < maxval
That will give you a single row containing the correct pricing level.
The second is to store NULL in that column instead of a string. A nullable numeric column will still work with a slight modification:
select pricing_level
from pricing_tbl
where product_id = '10'
and $market_val >= minval
and ($market_val < maxval or maxval is null)
In both those cases, you may want to translate the upper region (large number or null) to and from the word above when presenting or editing the table itself. This will make the process look the same even though the underlying data has changed.
i have 2 columns namely: Min and Max they are varchar
this is wrong. It should be int or at least decimal values
My query is something like this
this is wrong. Your query should include $marketval to do all the calcs on the database side.
here is an instance where the value of Max is above.
this is wrong. No numerical column should ever contain a string. you can store a big number there. or a NULL, but not a string.

AUTO_INCREMENT implementation for string column containing a special number format

Context and goal
In table clients I have a column clientNum CHAR(11) NOT NULL with UNIQUE KEY constraint. It contains client number in the format xxx-xxx-xxx where x is a decimal digit. For more details on the format see below.
I want to implement something like AUTO_INCREMENT for this column so that each client gets their number calculated automatically. From MySQL CREATE TABLE docs:
An integer or floating-point column can have the additional attribute AUTO_INCREMENT. When you insert a value of NULL (recommended) or 0 into an indexed AUTO_INCREMENT column, the column is set to the next sequence value. Typically this is value+1, where value is the largest value for the column currently in the table. AUTO_INCREMENT sequences begin with 1.
So I want to find the next number available and use it as clientNum value for newly inserted client row. Next number available is current maximum of clientNum incremented.
I’m coding in PHP using PDO to access the MySQL database (see PDO Tutorial for MySQL Developers).
Client number format
As stated above, the client number is in format xxx-xxx-xxx where x is a decimal digit. The range of each segment is 000 to 999. It is basically a 9-digit integer with leading zeroes and dash as thousands separator. It cannot get above 999-999-999.
Currently we want it be even more restricted, specifically in format 000-1xx-xxx (between 000-100-000 and 000-199-999). But there are already some numbers in the database that can start anywhere from 000-000-001 to 500-000-000.
Unfortunately it has to be stored in this format, I cannot change it.
Finding maximum
I need to get the max number in range 000-100-000 to 000-199-999, values outside this range must be ignored. This is where my problem comes in because as said before some numbers already exist above this.
Maximum is never 000-199-999. Otherwise in would result in adding 000-200-000 and the next time called maximum will be 000-199-999 again, resulting in attempt to insert 000-200-000 again.
How incrementation works
In PHP in can be done like this:
$clientNum = "000-100-000";
$clientNum = str_replace("-", "", $clientNum);
$clientNum++;
$clientNum = implode("-", str_split(str_pad($clientNum, 9, "0", STR_PAD_LEFT), 3));
Final $clientNum value is 000-100-001.
When the initial number is 000-120-015 then the code above produces 000-120-016. Overflow propagates to the next segment, i.e. 000-100-999 becomes 000-101-000. 999-999-999 cannot be incremented.
Idea to start with
In a loop I want to get the next number available, check if that number exists in the database, and if so, redo that loop until it finds an unused number. I know how to check if it’s in the database the first time, but I’m not sure how to do the loop.
Does anyone know a way to do this?
You may want to solve this in SQL, because otherwise you need two transactions (one for reading, one for writing) and meanwhile the number could be used by a concurrent access.
In MySQL, you can use this SQL reimplementation of your PHP code:
INSERT(INSERT(LPAD(CAST(CAST(REPLACE(clientNum, '-', '') as UNSIGNED) + 1 as CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-')
This increments 000-000-999 to 000-001-000 and 999-999-999 to 100-000-000 (truncated from 100-000-0000 by LPAD()). I warned you.
E.g. to just preview what the next value is, use
SELECT INSERT(INSERT(LPAD(CAST(CAST(REPLACE(clientNum, '-', '') as UNSIGNED) + 1 as CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-') FROM clients
If you want to use this when inserting a new row, it is used like this:
INSERT
INTO clients(clientNum, name)
SELECT
INSERT(INSERT(LPAD(CAST(
COALESCE(MAX(CAST(REPLACE(clientNum, '-', '') AS UNSIGNED)), 0) + 1
AS CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-'),
'John Doe'
FROM clients
This works regardless of what API you use to access the database, as long as it is MySQL database. The database does the computation. However, it does not work if clients is a temporary table, which I expect it not to be. More on that below.
See also string functions, CAST(), COALESCE() and INSERT … SELECT in MySQL manual.
Later you added that the permitted values are from range 000-100-000 to 000-199-999. Other values shall be ignored for the purpose of finding maximum. A WHERE clause must be added to the SELECT part of INSERT written above.
INSERT
INTO clients(clientNum, name)
SELECT
INSERT(INSERT(LPAD(CAST(
COALESCE(MAX(CAST(REPLACE(clientNum, '-', '') AS UNSIGNED)), 0) + 1
AS CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-'),
'John Doe'
FROM clients
WHERE clientNum BETWEEN '000-100-000' AND '000-199-999'
Then you stated that my solution does not work for you and proposed a supposed fix:
INSERT
INTO clients(clientNum, name)
VALUES
(SELECT
INSERT(INSERT(LPAD(CAST(
COALESCE(MAX(CAST(REPLACE(clientNum, '-', '') AS UNSIGNED)), 0) + 1
AS CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-')
FROM clients AS tmptable
WHERE clientNum BETWEEN '000-100-000' AND '000-199-999'),
'John Doe'
This uses a subquery instead of the INSERT … SELECT syntax.
In MySQL, table cannot be modified (by INSERT in this case) and read by a subquery at the same time. Quoting the subquery manual:
In MySQL, you cannot modify a table and select from the same table in a subquery. This applies to statements such as DELETE, INSERT, REPLACE, UPDATE, and (because subqueries can be used in the SET clause) LOAD DATA INFILE.
However, you found a workaround using a temporary table. A temporary table is used when an alias (in this case clients AS tmptable) is defined, which evades reading from and writing to the same table at the same time. You used temporary table to store the original table, the article describing the workaround uses it to store the result of the subquery (which is more efficient, I guess). Both approaches work.
At this point I want to point out that my solution should work (and works for me!) too except for the improbable case when clients is a temporary table. I think I can expect it not to be one. Quoting the INSERT … SELECT manual page:
When selecting from and inserting into a table at the same time, MySQL creates a temporary table to hold the rows from the SELECT and then inserts those rows into the target table. However, it remains true that you cannot use INSERT INTO t ... SELECT ... FROM t when t is a TEMPORARY table, because TEMPORARY tables cannot be referred to twice in the same statement (see Section C.5.7.2, “TEMPORARY Table Problems”).
As for me this is explicitly saying that my original approach using INSERT … SELECT should work.
Just to provide a complete answer, I’ll address your original request for PHP solution using database polling. Once more I must add that this is certainly not a good solution.
Your clientNum column must be a unique key. You need to repeat the following steps until successful update:
Get the current maximum of clientNum.
Increment the obtained value.
Try to insert the row.
If successful, finish, otherwise throw the clientNum max value away and loop.
The insertion will fail due to violation of the aforementioned unique key constraint. This happens when another connection to the database successfully performs an insert in the meantime between steps 1. and 3..
You should prepare the statement outside the loop using PDO::prepare() and then execute it in the loop. The return value of execute method indicates success (true) or failure (false).
This is enough info to implement step 3.. Steps 1. and 2. consist of fetching the result of
SELECT MAX(clientNum) FROM clients
and running it through the code provided by Stephanus Yanaputra. Step 4. is a simple loop condition using the return value from execution of INSERT query in step 3..
<?php
mysql_connect(....);
mysql_select_db($db_name);
$res=mysql_query("select ClientNum from ClientTable");
$name_arr=array();
while($row=mysql_fetch_array($res))
{
foreach($row as $name)
$name_arr[]=$name;
}
$clientNum="000-000-000";
while(true){
$clientNum = str_replace("-", "", $clientNum);
$clientNum++;
if($clientNum>999999999)
{
echo("No mismatch found");
break;
}
$clientNum = implode("-", str_split($clientNum, 3));
if(!in_array($clientNum, $name_arr))
{
echo "The first unmatched clientNum is:".$clientNum;//This is what you want.
break;
}
}
?>
Comments
The query execution only once i.e. outside the while loop makes it less time complex. The time complexity is reduced due to use of array instead of execution of the query itself multiple times because searching in an array is comparatively very less time complex than searching in the database.
An easy solution based on function that you provided.
Change the function name rawr() to any naming that you like. (I couldn't find the best name and ended up using some gibberish name lol).
function rawr($in)
{
$num = str_replace("-", "", $in);
$num++;
// Convert back
$str = (string) $num;
// Add Leading 0
while(strlen($str) < 9)
{
$str = "0" . $str;
}
echo $str . "<br />";
$final = substr($str,0,3) . "-" . substr($str,3,3) . "-" . substr($str,6,3);
return $final;
}
To test it, try this code:
echo rawr(0);
echo "<br />";
echo rawr("000-000-000");
echo "<br />";
echo rawr("012-345-678");
echo "<br />";
echo rawr("123-456-789");
echo "<br />";
This will give an output that you desire. However you will have to code it yourself to test the database. In my opinion, this is not the best way to solve your problem, but it should work :)

get max value in php (instead of mysql)

I have two msyql tables, Badges and Events. I use a join to find all the events and return the badge info for that event (title & description) using the following code:
SELECT COUNT(Badges.badge_ID) AS
badge_count,title,Badges.description
FROM Badges JOIN Events ON
Badges.badge_id=Events.badge_id GROUP
BY title ASC
In addition to the counts, I need to know the value of the event with the most entries. I thought I'd do this in php with the max() function, but I had trouble getting that to work correctly. So, I decided I could get the same result by modifying the above query by using "ORDER BY badgecount DESC LIMIT 1," which returns an array of a single element, whose value is the highest count total of all the events.
While this solution works well for me, I'm curious if it is taking more resources to make 2 calls to the server (b/c I'm now using two queries) instead of working it out in php. If I did do it in php, how could I get the max value of a particular item in an associative array (it would be nice to be able to return the key and the value, if possible)?
EDIT:
OK, it's amazing what a few hours of rest will do for the mind. I opened up my code this morning, and made a simple modification to the code, which worked out for me. I simply created a variable on the count field and, if the new one was greater than the old one, changed it to the new value (see the "if" statement in the following code):
if ( $c > $highestCount ) {
$highestCount = $c; }
This might again lead to a "religious war", but I would go with the two queries version. To me it is cleaner to have data handling in the database as much as possible. In the long run, query caching, etc.. would even out the overhead caused by the extra query.
Anyway, to get the max in PHP, you simply need to iterate over your $results array:
getMax($results) {
if (count($results) == 0) {
return NULL;
}
$max = reset($results);
for($results as $elem) {
if ($max < $elem) { // need to do specific comparison here
$max = $elem;
}
}
return $max;
}

Categories