0

I have two database tables that contain information about land contracts. They are related with land_contract_annual_price.land_contract_id -> land_contract.land_contract_id.

Table 'land_contract' enter image description here

Table 'land_contract_annual_price' enter image description here

If a land contract has the value "Rörligt pris" in the field land_contract_price_type, there are related values in the table land_contract_annual_price. At the moment I'm doing two queries, one to each table. I then merge the results and present the land contract as a nested JSON array like this:

Version 1

[  
 {  
  "land_contract_id":118,
  "land_contract_name":"Avtalsnamn",
  "location_id":71,
  "land_contract_link":"",
  "land_contract_notes":"",
  "land_owner_id":2,
  "land_contract_start_date":"2019-07-25",
  "land_contract_end_date":"2023-07-25",
  "land_contract_terminated":"false",
  "land_contract_payment_interval":"Halv\u00e5rsvis",
  "land_contract_price_type":"R\u00f6rligt \u00e5rspris",
  "land_contract_fixed_annual_price":null,
  "land_contract_annual_prices":[  
    {"year":1, "price":873.00},
    {"year":2, "price":77289.00},
    {"year":3, "price":8.00},
    {"year":4, "price":0.00},
    {"year":5, "price":8729.00}
  ]
 }
]

If a land contract has the value "Fast pris" in the field land_contract_price_type, there are no related values in the table land_contract_annual_price. In that case I present the land contract like this (without the extra array at the end):

Version 2

[
 {
  "land_contract_id":13,
  "land_contract_name":null,
  "location_id":null,
  "land_contract_link":"https:\/\/www.something.com\/preview\/Sl%C3%A4pvdam%20Edda\/Kddal\/Bddkta\/Besika%20Markavtal%20%20Halmstad%202016-03-08.pdf?role=personal",
  "land_contract_notes":"",
  "land_owner_id":null,
  "land_contract_start_date":"2016-03-08",
  "land_contract_end_date":"2026-03-08",
  "land_contract_terminated":"true",
  "land_contract_payment_interval":"\u00c5rsvis",
  "land_contract_price_type":"Fast \u00e5rspris",
  "land_contract_fixed_annual_price":"6000.00"
 }
]

What I didn't think of, is that this solution is bad when I'm fetchin ALL the land contracts. If I'm going to do a second query to another table whenever a land contract has the value "Rörligt pris" in the field land_contract_price_type, I'm going to do hundreds of extra queries.

Is there a way to create the nested JSON array with one (1) query when a land contract has the value "Rörligt pris" in the field land_contract_price_type?

Thanks!

Below is my current code.

function read($pdo, $Id = null, $ResponseMessage = null) {

    $params = [];
    $array = [];

    $sql = "SELECT  lc.Id, lc.Name, lc.LocationId, l.Name AS LocationName, lc.Notes, lc.LandOwnerId, lo.Name AS LandOwnerName, lc.StartDate, lc.EndDate, lc.IsTerminated, lc.PaymentInterval, lc.PriceType, lc.FixedAnnualPrice, lc.Link, lc.Created, lc.Updated, lcap.AnnualPriceYear AS Year, lcap.AnnualPriceAmount AS Amount
            FROM LandContract lc
            LEFT JOIN Location l ON l.Id = lc.LocationId
            LEFT JOIN LandOwner lo ON lo.Id = lc.LandOwnerId
            LEFT JOIN LandContractAnnualPrice lcap ON lcap.LandContractId = lc.Id  
            ORDER BY lc.Id  DESC, lcap.AnnualPriceYear DESC
            ";
    if ($Id) {
        $sql .= 'WHERE lc.Id = ?';
        $params[] = $Id;
    }

    echo $sql;

    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    while ($row = $stmt->fetch()) {
        // Fields we want to extract from the select statement into the array 
        $select_fields = ['Id', 'Name', 'LocationId', 'LocationName', 'Link', 'Notes', 'LandOwnerId', 'LandOwnerName',
                            'StartDate', 'EndDate', 'IsTerminated', 'PaymentInterval', 
                            'PriceType', 'FixedAnnualPrice ', 'Created', 'Updated'];

        if (!isset($array[$row['Id']])) {
            // initialize the subarray if it has not been set already 
            $array[$row['Id']] = array_intersect_key($row, array_flip($select_fields));

            if ($row['Year'] != null) {
                $array[$row['Id']]['AnnualPrices'] = [];
            } else {
                $array[$row['Id']]['AnnualPrice'] = $row['FixedAnnualPrice'];
            }
        }

        if ($row['Year'] != null) {
            $array[$row['Id']]['AnnualPrices'][] = ['Year' => $row['Year'], 'Amount' => $row['Amount']];
        }

    }

    if (empty($array)) {
        $ResponseMessage = new ResponseMessage();
        $ResponseMessage->Status = 'Error';
        $ResponseMessage->Message = 'No results';
        echo json_encode($ResponseMessage, JSON_UNESCAPED_UNICODE);
        exit;
    }

    $Response = array();

    if ($ResponseMessage) {
        $Response['Status'] = $ResponseMessage->Status;
        $Response['Message'] = $ResponseMessage->Message;
    }

    $Response['LandContracts'] = array_values($array);

    echo json_encode($Response, JSON_UNESCAPED_UNICODE);

    $stmt = null;
}
Rawland Hustle
  • 781
  • 1
  • 10
  • 16
  • It seems you would benefit from implementing [SQL joins](https://stackoverflow.com/questions/5706437/whats-the-difference-between-inner-join-left-join-right-join-and-full-join/6188334#6188334). Assuming you're using MySQL, you might want to use [JSON functions](https://dev.mysql.com/doc/refman/5.7/en/json-function-reference.html) in your query. – Ro Achterberg Jul 25 '19 at 20:14
  • @RoAchterberg Thanks for your answer. I used to have query with LEFT JOIN but I didn't know if, or how, I could get that JSON structure. Any suggestions? – Rawland Hustle Jul 25 '19 at 20:58
  • I suggest you take a look at the various `JSON_*` functions provided by MySQL to retrieve your result as a JSON-encoded representation of the column data. – Ro Achterberg Jul 25 '19 at 21:01
  • @RoAchterberg The JSON function did not work. Turns out my host is running Maria DB. I really thought it was MySQL. – Rawland Hustle Jul 26 '19 at 09:02
  • @RoAchterberg Is there a way to create the JSON I want with PHP (from one sql query)? – Rawland Hustle Jul 26 '19 at 09:07

1 Answers1

2

You are better off using a JOIN query, and then structure your array from the result - having a query within a loop is often a very bad idea, and an indicator that you can use a JOIN instead.

You want to use a LEFT JOIN, joining them on the land_contract_id in both tables.

Then loop your results, and construct your array, which you can end up encoding into a JSON string once done.

$params = [];
$array = [];

$sql = "SELECT lc.*, 
               py.land_contract_annual_price_year AS `year`,  
               py.land_contract_annual_price_amount AS `amount`
        FROM land_contract AS lc
        LEFT JOIN land_contract_annual_price AS py 
            ON py.land_contract_id = lc.land_contract_id
        ";
if (isset($_POST['land_contract_id'])) {
    $sql .= 'WHERE lc.land_contract_id = ?';
    $params[] = $_POST["land_contract_id"];
}

$stmt = $pdo->prepare($sql);
$stmt->execute($params);
while ($row = $stmt->fetch()) {
    // Fields we want to extract from the select statement into the array 
    $select_fields = ['land_contract_id', 'land_contract_name', 'location_id', 'land_contract_link', 'land_contract_notes', 'land_owner_id', 
                        'land_contract_start_date', 'land_contract_end_date', 'land_contract_terminated', 'land_contract_payment_interval', 
                        'land_contract_price_type', 'land_contract_fixed_annual_price '];

    if (!isset($array[$row['land_contract_id']])) {
        // initialize the subarray if it has not been set already 
        $array[$row['land_contract_id']] = array_intersect_key($row, array_flip($select_fields));

        if ($row['year'] != null) {
            $array[$row['land_contract_id']]['land_contract_annual_prices'] = [];
        } else {
            $array[$row['land_contract_id']]['land_contract_annual_price'] = $row['land_contract_fixed_annual_price'];
        }
    }

    if ($row['year'] != null) {
        $array[$row['land_contract_id']]['land_contract_annual_prices'][] = ['year' => $row['year'], 'amount' => $row['amount']];
    }

}

if (empty($array)) {
    echo "No results";
    exit;
}

echo json_encode($array, JSON_UNESCAPED_UNICODE);
Qirel
  • 25,449
  • 7
  • 45
  • 62
  • Thanks! PHP is complaining about this line: `$array[$row['land_contract_id']] = array_intersect_key($row, array_flip($select_fields);`. It says `"Parse error: syntax error, unexpected ';', expecting ')'`. – Rawland Hustle Jul 26 '19 at 11:49
  • Fixed it (added a paranthesis). I got another error. Let me check. – Rawland Hustle Jul 26 '19 at 11:50
  • Also changed `LEFT JOIN land_contract_annual_price_year` to `land_contract_annual_price`. Now it works! However, "You seem to want a LEFT JOIN, as you want results in land_contract regardless if there's a match in land_contract_annual_price or not." is not true. I did not know that LEFT JOIN worked like that. I only want the results if there **is** a match in land_contract_annual_price. Is that possible? – Rawland Hustle Jul 26 '19 at 11:54
  • Yes, then you want an `INNER JOIN` (which is the same as `JOIN`). – Qirel Jul 26 '19 at 11:56
  • 1
    As a sidenote, you don't have to name your columns with the prefix of the table-name. For example, `land_contract_annual_price_amount` is rather long - you can simply use `amount`. The schema then becomes `land_contract_annual_price.amount` (`table.column` instead of `table.table_column`), which is much easier to work with. – Qirel Jul 26 '19 at 12:01
  • I tried that, but it seems like I want something in between (which might not be possible). Look at my reference JSON structures in my original question. Basically, I want to echo ALL land contracts. But if a land contract *has* related values in table `land_contract_annual_price_year` I want them appended as an array at the end of the JSON (Version 1). If a land contract has *no* related values in table `land_contract_annual_price_year` I don't want an empty array at the end of the JSON (Version 2). Is this possible? – Rawland Hustle Jul 26 '19 at 12:03
  • Something like the above? Use a `LEFT JOIN`, and check if there was results in both table or not by checking of the value from the right-table (prices). – Qirel Jul 26 '19 at 12:07
  • It's the other way around. Now I get an (empty) array if there *no* related data, but no array if there is :D – Rawland Hustle Jul 26 '19 at 12:11
  • Whoops, inverted logic. Derp, hang on. – Qirel Jul 26 '19 at 12:13
  • You haven't fixed the logic, right? I'm not sure if I should wait or not :D – Rawland Hustle Jul 26 '19 at 12:41
  • I tried it and no land contract has the array at the end. That's why I was unsure if you were done or not. – Rawland Hustle Jul 26 '19 at 12:56
  • BTW, if I post a land_contract_id in order to fetch a single land contract, the line `$stmt = $pdo->prepare($sql);` causes `Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'land_contract_id' in where clause is ambiguous`. – Rawland Hustle Jul 26 '19 at 13:00
  • I changed `$sql .= 'WHERE land_contract_id = ?';` to `$sql .= 'WHERE lc.land_contract_id = ?';` and that solved it. The logic part is still wrong though :) – Rawland Hustle Jul 26 '19 at 13:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/197052/discussion-between-rawland-hustle-and-qirel). – Rawland Hustle Jul 26 '19 at 13:07
  • I have made some changes since we last spoke and adding the Id to the SQL query doesn't work anymore. See my current code in my original question. `if ($Id) { $sql .= 'WHERE lc.Id = ?'; $params[] = $Id; }`gets processed as `WHERE lc.Id = ?` in the SQL query. Why? – Rawland Hustle Aug 18 '19 at 19:12
  • Its because you don't really see the final querystring - the placeholders `?` will only get populated in the second roundtrip to the database (it first does the query with placeholders, then sends the data). If you can have multiple conditions in the `WHERE` clause, its better to put them in and array, `implode()` it on `AND` and prepend `WHERE` to the string before putting it into the query. – Qirel Aug 18 '19 at 19:46