17

Problem:

I have written a function in my model to insert an order into my database. I am using transactions to make sure that everything commits or else it will be rolled back.

My problem is that CodeIgniter is not showing any database errors, however it is rolling back the transaction but then returning TRUE for trans_status. However, this only happens if there is a discount on the order. If there is no discount on the order, everything commits and works properly.

I am currently using CodeIgniter 3.19, PHP (7.2), mySQL (5.7), and Apache 2.4. (Working on Ubuntu 18.04)

The function logic works as such:

  • Inserts the order array into tbl_orders
  • Saves order_id, and goes through each of the order products (attaches order_id) and inserts the product in tbl_order_products,
  • Saves order_product_id and attaches it to an array of users attendance options and inserts that into tbl_order_attendance
  • Takes the payment transaction array (attaches the order_id) and inserts that into tbl_transactions
  • IF there is a discount on the order, it decreases the discount_redeem_count (number of redeemable discount codes) by 1.

Actual Function

[Function]:

public function add_order(Order $order, array $order_products, Transaction $transaction = NULL){
  $this->db->trans_start();

  $order->create_order_code();
  $order_array = $order->create_order_array();

  $this->db->insert('tbl_orders', $order_array);
  $order_id = $this->db->insert_id();
  $new_order = new Order($order_id);

  foreach($order_products as $key=>$value){
    $order_products[$key]->set_order($new_order);
    $order_product_array = $order_products[$key]->create_order_product_array();

    $this->db->insert('tbl_order_products', $order_product_array);
    $order_product_id = $this->db->insert_id();

    $product = $order_products[$key]->get_product();

    switch ($product->get_product_class()){
        case 'Iteration':
            $this->db->select('module_id, webcast_capacity, in_person_capacity');
            $this->db->from('tbl_modules');
            $this->db->where('iteration_id', $product->get_product_class_id());
            $results = $this->db->get()->result_array();
            break;
        case 'Module':
            $this->db->select('module_id, webcast_capacity, in_person_capacity');
            $this->db->from('tbl_modules');
            $this->db->where('module_id', $product->get_product_class_id());
            $results = $this->db->get->result_array();
            break;
      }

      if(!empty($results)){
        foreach($results as $result){
        $module_id = $result['module_id'];

        if($result['webcast_capacity'] !== NULL && $result['in_person_capacity'] !== NULL){
          $attendance_method = $order_products[$key]->get_attendance_method();
        }elseif($result['webcast_capacity'] !== NULL && $result['in_person_capacity'] === NULL){
          $attendance_method = 'webcast';
        }elseif($result['webcast_capacity'] === NULL && $result['in_person_capacity'] !== NULL){
          $attendance_method = 'in-person';
        }

        $order_product_attendance_array = array(
          'order_product_id' => $order_product_id,
          'user_id' => $order_products[$key]->get_customer(true),
          'module_id' => $module_id,
          'attendance_method' => $attendance_method,
        );

        $order_product_attendance[] = $order_product_attendance_array;
      }
      $this->db->insert_batch('tbl_order_product_attendance', $order_product_attendance);
    }

    if(!empty($order_products[$key]->get_discount())){
      $discount = $order_products[$key]->get_discount();
    }
  }

  if(!empty($transaction)){
    $transaction->set_order($new_order);
    $transaction_array = $transaction->create_transaction_array();
    $this->db->insert('tbl_transactions', $transaction_array);
    $transaction_id = $this->db->insert_id();
  }

  if(!empty($discount)){
    $this->db->set('discount_redeem_count', 'discount_redeem_count-1', false);
    $this->db->where('discount_id', $discount->get_discount_id());
    $this->db->update('tbl_discounts');
  }

  if($this->db->trans_status() !== false){
    $result['outcome'] = true;
    $result['insert_id'] = $order_id;
    return $result;
  }else{
    $result['outcome'] = false;
    return $result;
  }
}

When this function completes with a discount, both trans_complete and trans_status return TRUE. However the transaction is never committed.

What I've tried:

  • I have dumped the contents of $this->db->error() after each query and there are no errors in any of the queries.

  • I have used this->db->last_query() to print out each query and then checked the syntax online to see if there were any problems, there were none.

  • I also tried changing to using CodeIgniters Manual Transactions like:

[Example]

$this->db->trans_begin();
 // all the queries
if($this->db->trans_status() !== false){
    $this->db->trans_commit();
    $result['outcome'] = true;
    $result['insert_id'] = $order_id;
    return $result;
}else{
    $this->db->trans_rollback();
    $result['outcome'] = false;
    return $result;
}
  • I have tried echoing and var_dumping all of the return insert_ids and they all work, I have also outputted the affected_rows() of the UPDATE query and it is showing that 1 row was updated. However, still nothing being committed:

[Values Dumped]

int(10) // order_id
int(10) // order_product_id
array(3) { 
    ["module_id"]=> string(1) "1" 
    ["webcast_capacity"]=> string(3) "250" 
    ["in_person_capacity"]=> string(3) "250" } // $results array (modules)

array(1) { 
    [0]=> array(4) { 
        ["order_product_id"]=> int(10 
        ["user_id"]=> string(1) "5" 
        ["module_id"]=> string(1) "1" 
        ["attendance_method"]=> string(7) "webcast" } } // order_product_attendance array

int(9) // transaction_id
int(1) // affected rows
string(99) "UPDATE `tbl_discounts` 
            SET discount_redeem_count = discount_redeem_count- 1 
            WHERE `discount_id` = 1" // UPDATE query

- I have also tried replacing the last UPDATE query with a completely different one that tries to update a different table with different values. That query ALSO did not work, which makes me think that I am hitting some sort of memory limit with the transaction. However, when monitoring mysqld processes, none of them seem to spike or have difficulty.

  • I have tried submitting an order that doesn't have a discount and the entire process works! Which leads me to believe that my problem is with my UPDATE query. [After Update:] But it seems that the update query is working as well.

Suggestions Tried:

  • We have tried setting log_threshold to 4, and looked through the CodeIgniter Log Files which shows no history of a rollback.

  • We have checked the mySQL Query Log:

[Query Log]

2018-12-03T15:20:09.452725Z         3 Query     UPDATE `tbl_discounts` SET discount_redeem_count = discount_redeem_count-1 WHERE `discount_id` = '1'
2018-12-03T15:20:09.453673Z         3 Quit

It shows that a QUIT command is being sent directly after the UPDATE query. This would initiate a rollback, however the trans_status is returning TRUE.

I also changed my my.cnf file for mySQL to have innodb_buffer_pool_size=256M and innodb_log_file_size=64M. There was no change in the outcome.

  • As @ebcode recommended, I changed UPDATE query to use a simple_query() instead of using default methods from CodeIgniter's Query Builder Class:

[Simple Query]

if(!empty($discount)){
    $this->db->simple_query('UPDATE `tbl_discounts` SET '.
    'discount_redeem_count = discount_redeem_count-1 WHERE '.
    '`discount_id` = \''.$discount['discount_id'].'\'');
}

However, this produced did not affect the outcome any differently.

If you have an idea that I haven't tried yet, or need more information from me, please comment and I will reply promptly.

Question:

Why does trans_status return TRUE if none of my transaction is being committed?

In order to try and bring some clarity to users just finding this question now, the latest updates to the post will appear in italics *

adamoffat
  • 639
  • 6
  • 22
  • did you check $discount['discount_id'] values? – Vickel Nov 30 '18 at 22:47
  • @Vickel Hey, yeah I did. When I used ‘$this->db->last_query()’ it shows that the ‘discount_id’ value is included. (In my test the ID value is 1) – adamoffat Nov 30 '18 at 22:49
  • You haven't run `$this->db->trans_strict(FALSE);` somewhere you're not showing us have you? – DFriend Nov 30 '18 at 23:36
  • @DFriend I appreciate the comment! Absolutely not, that was one of the first things I checked for. – adamoffat Nov 30 '18 at 23:37
  • There are no entries in the log files either? – DFriend Nov 30 '18 at 23:41
  • @DFriend I have the log file, but I see no errors being reported. Log threshold at 4 – adamoffat Nov 30 '18 at 23:56
  • There should be errors logged if things are being rolled back. – DFriend Dec 01 '18 at 00:07
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/184540/discussion-between-adamoffat-and-dfriend). – adamoffat Dec 01 '18 at 00:07
  • can you show full code as you say you have reduced it. there must be an issue somewhere. stupid question maybe, but sometimes people have multiple databases, are you sure you are checking the correct one? – Alex Dec 01 '18 at 02:44
  • @Alex This is all one database, I could post my full code, but it's full of objects that you guys wouldn't know about. Do you still think it's worth while? – adamoffat Dec 01 '18 at 02:49
  • I'm confused. In Edit 1 you say, "...the `order_id` is still being stored and kept, as well as the `order_product_id` and the `transaction_id`". These things would not be kept unless the transaction had been committed. What are you seeing (or not seeing) that has you saying there has been no commit? – DFriend Dec 01 '18 at 14:07
  • @DFriend sorry, they are being stored into the variables before the rollback takes place. And I can echo those variables and see the values. – adamoffat Dec 01 '18 at 17:02
  • And then you don't find a record in `'tbl_orders'` with a value === `$order_id`? – DFriend Dec 01 '18 at 17:05
  • @DFriend No, this is why I am thinking it must be that last `UPDATE` query. However I don’t know why there’s no record of a rollback with in the logs. – adamoffat Dec 01 '18 at 17:06
  • @DFriend The thing is, when I use the exact same function without a discount. Everything commits and works. So I think the problem has to be something to do with the last query. – adamoffat Dec 01 '18 at 17:16
  • You're 100% certain that `$discount['discount_id']` will ALWAYS be set and will ALWAYS be found 'tbl_discounts'? – DFriend Dec 01 '18 at 17:23
  • @DFriend Positive, when I output `$this->db->last_query()` it’s shows that the `discount_id` in the query is 1. Which matches the `discount_id` I am looking for in my database. However, I haven’t found any evidence of the `discount_redeem_count` being reduced by one. – adamoffat Dec 01 '18 at 17:26
  • @adamoffat check this https://stackoverflow.com/questions/15224826/codeigniter-transactions/34695559#34695559 – Abdulla Nilam Dec 05 '18 at 05:55
  • @AbdullaNilam Thanks for the comment. This post was one of the first I looked at. However it provided me no information I didn’t already have. – adamoffat Dec 05 '18 at 13:59
  • Can you show the last query being run after the discount update. And also the $this->db->affected_rows() result. – reignsly Dec 06 '18 at 04:25
  • Also try to remove the backticks using this `$this->db->_protect_identifiers=false;` add this before the update query – reignsly Dec 06 '18 at 04:32
  • @reignsly Question has been updated, I totally forgot about `affected_rows`, it's showing that the field was updated but its still rolling back and returning a false positive to `trans_status()` and `trans_complete()` – adamoffat Dec 06 '18 at 14:13
  • Sorry if my question may sound naive or i haven't read your code carefully but does the update procedure depend on the previous procedures? or is it a separate query IF there is a discount cause i may have a solution for both situations but i hope it doesn't depend on the previous procedures? – Sherif Salah Dec 11 '18 at 20:18
  • @SherifSalah Hey, thanks for the comment. The discount is dependent on the other procedures as the `discount_redeem_count` should only be reduced by one IF there is a discount on the order. – adamoffat Dec 11 '18 at 20:19

6 Answers6

5

I found my problem. I want to say Thank-you to everyone that tried to help however, this one was my fault.

Earlier in the Controller method that calls this function I called another function that starts a transaction. That transaction was never closed and therefore continued on into this new transaction.

Because the transaction just wasn’t committed and there were no errors, I was not able to find any errors or any history of a rollback. However as soon as I closed the previous transaction everything worked.

There was no evidence of any problems in the mySQL query log, mySQL error log, or the CodeIgniter error logs. I was only able to find this problem by slowly reading through the entire mySQL query log.

For anyone who comes across this problem: Check your other transactions and make sure that they are all closed.

adamoffat
  • 639
  • 6
  • 22
  • 1
    Not committing your transaction is not an error. As such, you will not find errors in the logs. The best way is to always type your transaction commit statement whenever you start a transaction. Then fill between them your logic. This mistake is very common and I am no exception :) – nazim Dec 16 '18 at 16:34
0

Maybe try replacing your update code with a call to simple_query?

Change:

if(!empty($discount)){
    $this->db->set('discount_redeem_count', 'discount_redeem_count-1', false);
    $this->db->where('discount_id', $discount['discount_id']);
    $this->db->update('tbl_discounts');
}

To:

if(!empty($discount)){
    $this->db->simple_query('UPDATE `tbl_discounts` SET '.
    'discount_redeem_count = discount_redeem_count-1 WHERE '.
    '`discount_id` = \''.$discount['discount_id'].'\'');
}

I poked around the CodeIgniter source code a bit, and it looks like the default query function does a lot of housekeeping that might be messing things up. And the simple_query function has this morsel of documentation:

/**
 * Simple Query
 * This is a simplified version of the query() function. Internally
 * we only use it when running transaction commands since they do
 * not require all the features of the main query() function.
 *
 * @param   string  the sql query
 * @return  mixed
 */
ebcode
  • 124
  • 1
  • 5
0

I'm thinking about your database field discount_redeem_count name.

Are you sure discount_redeem_count is not number? because here your trying to push a string value. So database field should be var or text.

Maybe helpful.

Thanks.

dmarcosl
  • 15
  • 6
Jayesh Naghera
  • 319
  • 3
  • 13
  • Thanks for the answer! However, all of my other foreign keys are done using the same method. Using a string to query an integer value from the database has never caused me any problems before. – adamoffat Dec 04 '18 at 14:08
  • Not sure.. Maybe cache issue, Use $this->db->cache_delete_all(); before update query. – Jayesh Naghera Dec 05 '18 at 05:22
0

First, you should make sure to turn on the Transaction Strict mode before starting transactions.

$this->db->trans_strict(TRUE);
$this->db->trans_start();

Second, please check the $order variable. is this an array? or a class? if this is a class then it probably failed at this line

$this->db->insert('tbl_orders', $order);

Third, if $order variable is a class then this line will succeed. If $order variable is an array then this line will fail.

$discount = $order->get_discount();

McBern
  • 549
  • 1
  • 4
  • 8
  • Hey there, At first I didn't include full code as I thought my objects might be confusing however I see now that the opposite is true, I have updated my question to show full code w/ objects. Also from CodeIgniter Docs: "By default CodeIgniter runs all transactions in Strict Mode". – adamoffat Dec 05 '18 at 14:31
  • I think your discount object may fail in the first pass if the order has no discount. Because it was not initialized. – McBern Dec 06 '18 at 00:32
  • Hey! So not every order will have a discount. However the function checks both times to make sure there is a discount before using it to update. Also, when I check the SQL query using `$this->db->last_query()` it outputs a full and syntactic query that can be used in phpMyAdmin. – adamoffat Dec 06 '18 at 00:36
0

Based on EDIT 5:

This

$this->db->set('discount_redeem_count', 'discount_redeem_count-1', false);

should work (the back-ticked one would not.. the whole point of passing the false third parameter is so that CI doesn't escape your parameters with backticks, which would prevent the set statement to be passed as a string.

I did some quick tests in my own development code with an update similar to that one and the only way I got it to fail was to alter the table being updated so that the field (discount_redeem_count in your case) wasn't numeric. If my field was, for example, a VARCHAR it wouldn't work, but when I tried it on an INT field, it worked with no issues.

Are you positive the discount_redeem_count field is numeric?

Javier Larroulet
  • 3,047
  • 3
  • 13
  • 30
  • Hey, thanks for the answer! I only tried to insert back ticks on that `UPDATE` query as I saw it on this [answer](https://stackoverflow.com/questions/12904308/update-query-increment-field-plus-1-codeigniter). Also, I have checked the mySQL field data type and it is an `INT` value. – adamoffat Dec 05 '18 at 15:46
  • 1
    bummer... I can't seem to find any other way to make it fail :( I'll update if I do – Javier Larroulet Dec 05 '18 at 16:56
0

(Both of these suggestions were tried, to no avail.)

Suggestion 1

Perhaps this is the real answer:

trans_status() must be run when you're inside a transaction. In your example trans_complete() resets the status flag.

(However, this is sad if you are using Galera or Group Replication since a transaction can still fail when running COMMIT.)

Suggestion 2

These are  `== NULL`:  NULL, '', FALSE, 0
These are `!== NULL`:        '', FALSE, 0

Notice how you use the "triple" !== for some tests against NULL, but use the "double" == for others.

Do this to see what you are actually getting:

var_dump($result['webcast_capacity']);
Rick James
  • 135,179
  • 13
  • 127
  • 222
  • Hey there, so I have dumped the values of pretty much everything and it's all working. The arrays being inserted all return `insert_ids`. However, none of the transaction is actually committed and the `trans_status` returns `TRUE`. I can include a dump of all the arrays in the question if that will make it more clear. – adamoffat Dec 11 '18 at 20:17
  • @adamoffat - `trans_status` must be called _before_ `trans_complete`. (See the added text.) – Rick James Dec 11 '18 at 20:58
  • I saw that in the other answer, however [CodeIgniters documentation](https://www.codeigniter.com/user_guide/database/transactions.html) contradicts it. Anyways, I tried to do that but there was no change in the outcome of the function. :( – adamoffat Dec 12 '18 at 14:35
  • You are correct about the comparison operators and I have updated to make sure it is consistent. I am using strict comparisons on purpose. – adamoffat Dec 13 '18 at 18:46