1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @author Bart Visscher <[email protected]> |
4
|
|
|
* @author Joas Schilling <[email protected]> |
5
|
|
|
* @author Jonny007-MKD <[email protected]> |
6
|
|
|
* @author Morris Jobke <[email protected]> |
7
|
|
|
* @author Robin Appelman <[email protected]> |
8
|
|
|
* @author Thomas Müller <[email protected]> |
9
|
|
|
* @author Tom Needham <[email protected]> |
10
|
|
|
* |
11
|
|
|
* @copyright Copyright (c) 2017, ownCloud GmbH |
12
|
|
|
* @license AGPL-3.0 |
13
|
|
|
* |
14
|
|
|
* This code is free software: you can redistribute it and/or modify |
15
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
16
|
|
|
* as published by the Free Software Foundation. |
17
|
|
|
* |
18
|
|
|
* This program is distributed in the hope that it will be useful, |
19
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
20
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
21
|
|
|
* GNU Affero General Public License for more details. |
22
|
|
|
* |
23
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
24
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
25
|
|
|
* |
26
|
|
|
*/ |
27
|
|
|
|
28
|
|
|
namespace OC\DB; |
29
|
|
|
use Doctrine\DBAL\Exception\DriverException; |
30
|
|
|
use Doctrine\DBAL\Exception\UniqueConstraintViolationException; |
31
|
|
|
use Doctrine\DBAL\Platforms\OraclePlatform; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* This handles the way we use to write queries, into something that can be |
35
|
|
|
* handled by the database abstraction layer. |
36
|
|
|
*/ |
37
|
|
|
class Adapter { |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var \OC\DB\Connection $conn |
41
|
|
|
*/ |
42
|
|
|
protected $conn; |
43
|
|
|
|
44
|
|
|
public function __construct($conn) { |
45
|
|
|
$this->conn = $conn; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @param string $table name |
50
|
|
|
* @return int id of last insert statement |
51
|
|
|
*/ |
52
|
|
|
public function lastInsertId($table) { |
53
|
|
|
return $this->conn->realLastInsertId($table); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @param string $statement that needs to be changed so the db can handle it |
58
|
|
|
* @return string changed statement |
59
|
|
|
*/ |
60
|
|
|
public function fixupStatement($statement) { |
61
|
|
|
return $statement; |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Create an exclusive read+write lock on a table |
66
|
|
|
* |
67
|
|
|
* @param string $tableName |
68
|
|
|
* @since 9.1.0 |
69
|
|
|
*/ |
70
|
|
|
public function lockTable($tableName) { |
71
|
|
|
$this->conn->beginTransaction(); |
72
|
|
|
$this->conn->executeUpdate('LOCK TABLE `' .$tableName . '` IN EXCLUSIVE MODE'); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Release a previous acquired lock again |
77
|
|
|
* |
78
|
|
|
* @since 9.1.0 |
79
|
|
|
*/ |
80
|
|
|
public function unlockTable() { |
81
|
|
|
$this->conn->commit(); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Insert a row if the matching row does not exists. |
86
|
|
|
* |
87
|
|
|
* @param string $table The table name (will replace *PREFIX* with the actual prefix) |
88
|
|
|
* @param array $input data that should be inserted into the table (column name => value) |
89
|
|
|
* @param array|null $compare List of values that should be checked for "if not exists" |
90
|
|
|
* If this is null or an empty array, all keys of $input will be compared |
91
|
|
|
* Please note: text fields (clob) must not be used in the compare array |
92
|
|
|
* @return int number of inserted rows |
93
|
|
|
* @throws \Doctrine\DBAL\DBALException |
94
|
|
|
*/ |
95
|
|
|
public function insertIfNotExist($table, $input, array $compare = null) { |
96
|
|
|
if (empty($compare)) { |
97
|
|
|
$compare = array_keys($input); |
98
|
|
|
} |
99
|
|
|
$query = 'INSERT INTO `' . $table . '` (`' |
100
|
|
|
. implode('`,`', array_keys($input)) . '`) SELECT ' |
101
|
|
|
. str_repeat('?,', count($input) - 1) . '? ' // Is there a prettier alternative? |
102
|
|
|
. 'FROM `' . $table . '` WHERE '; |
103
|
|
|
|
104
|
|
|
$inserts = array_values($input); |
105
|
|
View Code Duplication |
foreach ($compare as $key) { |
|
|
|
|
106
|
|
|
$query .= '`' . $key . '`'; |
107
|
|
|
if (is_null($input[$key])) { |
108
|
|
|
$query .= ' IS NULL AND '; |
109
|
|
|
} else { |
110
|
|
|
$inserts[] = $input[$key]; |
111
|
|
|
$query .= ' = ? AND '; |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
$query = substr($query, 0, strlen($query) - 5); |
115
|
|
|
$query .= ' HAVING COUNT(*) = 0'; |
116
|
|
|
return $this->conn->executeUpdate($query, $inserts); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Inserts, or updates a row into the database. Returns the inserted or updated rows |
121
|
|
|
* @param $table string table name including **PREFIX** |
122
|
|
|
* @param $input array the key=>value pairs to insert into the db row |
123
|
|
|
* @param $compare array columns that should be compared to look for existing arrays |
124
|
|
|
* @return int the number of rows affected by the operation |
125
|
|
|
* @throws DriverException|\RuntimeException |
126
|
|
|
*/ |
127
|
|
|
public function upsert($table, $input, $compare) { |
128
|
|
|
|
129
|
|
|
$this->conn->beginTransaction(); |
130
|
|
|
$done = false; |
131
|
|
|
|
132
|
|
|
if (empty($compare)) { |
133
|
|
|
$compare = array_keys($input); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
// Construct the update query |
137
|
|
|
$qbu = $this->conn->getQueryBuilder(); |
138
|
|
|
$qbu->update($table); |
139
|
|
|
foreach($input as $col => $val) { |
140
|
|
|
$qbu->set($col, $qbu->createParameter($col)) |
141
|
|
|
->setParameter($col, $val); |
142
|
|
|
} |
143
|
|
|
foreach($compare as $key) { |
144
|
|
|
if (is_null($input[$key]) || ($input[$key] === '' && $this->conn->getDatabasePlatform() instanceof OraclePlatform)) { |
145
|
|
|
$qbu->andWhere($qbu->expr()->isNull($key)); |
146
|
|
|
} else { |
147
|
|
|
if($this->conn->getDatabasePlatform() instanceof OraclePlatform) { |
148
|
|
|
$qbu->andWhere( |
149
|
|
|
$qbu->expr()->eq( |
150
|
|
|
// needs to cast to char in order to compare with char |
151
|
|
|
$qbu->createFunction('to_char(`'.$key.'`)'), // TODO does this handle empty strings on oracle correclty |
152
|
|
|
$qbu->expr()->literal($input[$key]))); |
153
|
|
|
} else { |
154
|
|
|
$qbu->andWhere( |
155
|
|
|
$qbu->expr()->eq( |
156
|
|
|
$key, |
157
|
|
|
$qbu->expr()->literal($input[$key]))); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
// Construct the insert query |
163
|
|
|
$qbi = $this->conn->getQueryBuilder(); |
164
|
|
|
$qbi->insert($table); |
165
|
|
|
foreach($input as $c => $v) { |
166
|
|
|
$qbi->setValue($c, $qbi->createParameter($c)) |
167
|
|
|
->setParameter($c, $v); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
$rows = 0; |
171
|
|
|
$count = 0; |
172
|
|
|
// Attempt 5 times before failing the upsert |
173
|
|
|
$maxTry = 5; |
174
|
|
|
|
175
|
|
|
while(!$done && $count < $maxTry) { |
176
|
|
|
try { |
177
|
|
|
// Try to update |
178
|
|
|
$rows = $qbu->execute(); |
179
|
|
|
} catch (DriverException $e) { |
180
|
|
|
// Skip deadlock and retry |
181
|
|
|
// @TODO when we update to DBAL 2.6 we can use DeadlockExceptions here |
182
|
|
|
if($e->getErrorCode() == 1213) { |
183
|
|
|
$count++; |
184
|
|
|
continue; |
185
|
|
|
} else { |
186
|
|
|
// We should catch other exceptions up the stack |
187
|
|
|
$this->conn->rollBack(); |
188
|
|
|
throw $e; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
if($rows > 0) { |
192
|
|
|
// We altered some rows, return |
193
|
|
|
$done = true; |
194
|
|
|
} else { |
195
|
|
|
// Try the insert |
196
|
|
|
$this->conn->beginTransaction(); |
197
|
|
|
try { |
198
|
|
|
// Execute the insert query |
199
|
|
|
$rows = $qbi->execute(); |
200
|
|
|
$done = $rows > 0; |
201
|
|
|
} catch (UniqueConstraintViolationException $e) { |
202
|
|
|
// Catch the unique violation and try the loop again |
203
|
|
|
$count++; |
204
|
|
|
} |
205
|
|
|
// Other exceptions are not caught, they should be caught up the stack |
206
|
|
|
$this->conn->commit(); |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
// Pass through failures correctly |
211
|
|
|
if($count === $maxTry) { |
212
|
|
|
$params = implode(',', $input); |
213
|
|
|
$updateQuery = $qbu->getSQL(); |
214
|
|
|
$insertQuery = $qbi->getSQL(); |
215
|
|
|
throw new \RuntimeException("DB upsert failed after $count attempts. UpdateQuery: $updateQuery InsertQuery: $insertQuery"); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
$this->conn->commit(); |
219
|
|
|
return $rows; |
220
|
|
|
|
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.