ChadoRecord.inc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. <?php
  2. /**
  3. * Provide a class for basic querying of Chado; specifically, select, insert, update and delete.
  4. *
  5. * Eventually this class is meants to replace the existing chado_select_record(),
  6. * chado_insert_record(), chado_update_record() and chado_delete_record() API
  7. * functions to create a cleaner, more maintainable and more easily tested
  8. * interface to querying Chado.
  9. *
  10. * @todo Add documentation for save() and delete().
  11. *
  12. * Basic Usage:
  13. * - Select/Find
  14. * The following example selects an organism with the scientific name
  15. * "Tripalus databasica" from the organism table of Chado.
  16. * @code
  17. // First we create an instance of the ChadoRecord class
  18. // specifying the table we want to query.
  19. $record = new \ChadoRecord('organism');
  20. // Next we indicate the values we know.
  21. $record->setValues([
  22. 'genus' => 'Tripalus',
  23. 'species' => 'databasica',
  24. ]);
  25. // And finally we simply ask the class to find the chado record
  26. // we indicated when we set the values above.
  27. $success = $record->find();
  28. if ($success) {
  29. // Retrieve the values if we were successful in finding the record.
  30. $result = $record->getValues();
  31. }
  32. * @endcode
  33. * - Insert:
  34. * The following example inserts a sample record into the stock table.
  35. * @code
  36. // First we create an instance of the ChadoRecord class
  37. // specifying the table we want to query.
  38. $record = new \ChadoRecord('stock');
  39. // Next we indicate the values we know.
  40. $record->setValues([
  41. 'name' => 'My Favourite Plant',
  42. 'uniquename' => 'Plectranthus scutellarioides Trailing Plum Brocade',
  43. 'organism_id' => [ 'genus' => 'Tripalus', 'species' => 'databasica' ],
  44. 'type_id' => [ 'name' => 'sample', 'cv_id' => [ 'name' => 'Sample processing and separation techniques' ] ],
  45. ]);
  46. // And finally, we ask the class to insert the chado record
  47. // we described when we set the values above.
  48. $result = $record->insert();
  49. * @endcode
  50. * - Update:
  51. * The following example updates the "Tripalus databasica" record to specify the common name.
  52. * @code
  53. // For brevity we're going to hardcode the original record
  54. // including the id although you would Never do this in practice.
  55. // Rather you would first find the record as shown in a previous example.
  56. $original_record = [
  57. 'organism_id' => 1,
  58. 'genus' => 'Tripalus',
  59. 'species' => 'databasica',
  60. ];
  61. // First we create an instance of the ChadoRecord class
  62. // specifying the table we want to query.
  63. // NOTICE: this time we set the record_id when creating the instance.
  64. $record = new \ChadoRecord('organism', $original_record['organism_id']);
  65. // Now we set the values we want to change.
  66. $record->setValues([
  67. 'common_name' => 'Tripal',
  68. ]);
  69. // And then tell the class to update the record.
  70. $record->update();
  71. * @endcode
  72. */
  73. class ChadoRecord {
  74. /**
  75. * @var string
  76. * Holds the name of the table that this record belogns to.
  77. */
  78. protected $table_name = '';
  79. /**
  80. * @var array
  81. * Holds the Drupal schema definition for this table.
  82. */
  83. protected $schema = [];
  84. /**
  85. * @var array
  86. * Holds the values for the columns of the record
  87. */
  88. protected $values = [];
  89. /**
  90. * @var array
  91. * An array of required columns.
  92. */
  93. protected $required_cols = [];
  94. /**
  95. * @var boolean
  96. * An array of required columns which have yet to be set.
  97. */
  98. protected $missing_required_col = [];
  99. /**
  100. * @var integer
  101. * The numeric Id for this record.
  102. */
  103. protected $record_id = NULL;
  104. /**
  105. * @var string
  106. * The column name for the primary key.
  107. */
  108. protected $pkey = '';
  109. /**
  110. * The list of column names in the table.
  111. * @var array
  112. */
  113. protected $column_names = [];
  114. /**
  115. * The ChadoRecord constructor
  116. *
  117. * @param string $table_name
  118. * The name of the table that the record belongs to.
  119. *
  120. * @param string $record_id
  121. * An optional record ID if this record is already present in Chado.
  122. */
  123. public function __construct($table_name, $record_id = NULL) {
  124. if (!$table_name) {
  125. $message = t('ChadoRecord::_construct(). The $table_name argument is required for a ChadoRecord instance.');
  126. throw new Exception($message);
  127. }
  128. // Set the table name and schema.
  129. $this->table_name = $table_name;
  130. $this->schema = chado_get_schema($this->table_name);
  131. if (!$this->schema) {
  132. $message = t('ChadoRecord::_construct(). Could not find a matching table schema in Chado for the table: !table.',
  133. ['!table' => $this->table_name]);
  134. throw new Exception($message);
  135. }
  136. // Chado tables never have more than one column as a primary key so
  137. // we are good just getting the first element.
  138. $this->pkey = $this->schema['primary key'][0];
  139. // Save the column names.
  140. foreach ($this->schema['fields'] as $column_name => $col_details) {
  141. $this->column_names[] = $column_name;
  142. }
  143. // Get the required columns.
  144. foreach ($this->schema['fields'] as $column => $col_schema) {
  145. foreach ($col_schema as $param => $val) {
  146. if (preg_match('/not null/i', $param) and $col_schema[$param]) {
  147. $this->required_cols[] = $column;
  148. }
  149. }
  150. }
  151. // Currently all required columns are missing.
  152. $this->missing_required_col = $this->required_cols;
  153. // If a record_id was provided then lookup the record and set the values.
  154. if ($record_id) {
  155. try {
  156. $sql = 'SELECT * FROM {' . $this->table_name . '} WHERE ' . $this->pkey . ' = :record_id';
  157. $result = chado_query($sql, [':record_id' => $record_id]);
  158. $values = $result->fetchAssoc();
  159. if (empty($values)) {
  160. $message = t('ChadoRecord::_construct(). Could not find a record in table, !table, with the given !pkey: !record_id.',
  161. ['!pkey' => $this->pkey, '!record_id' => $record_id, '!table' => $this->table_name]);
  162. throw new Exception($message);
  163. }
  164. $this->record_id = $record_id;
  165. $this->values = $values;
  166. }
  167. catch (Exception $e) {
  168. $message = t('ChadoRecord::_construct(). Could not find a record in table, !table, with the given !pkey: !record_id. ERROR: !error',
  169. ['!pkey' => $this->pkey, '!record_id' => $record_id, '!table' => $this->table_name, '!error' => $e->getMessage()]);
  170. throw new Exception($message);
  171. }
  172. }
  173. }
  174. /**
  175. * Retrieves the record ID.
  176. *
  177. * @return number
  178. */
  179. public function getID() {
  180. return $this->record_id;
  181. }
  182. /**
  183. * Retrieves the table name.
  184. *
  185. * @return string
  186. * The name of the table that the record belongs to.
  187. */
  188. public function getTable() {
  189. return $this->table_name;
  190. }
  191. /**
  192. * Retrieves the table schema.
  193. *
  194. * @return array
  195. * The Drupal schema array for the table.
  196. */
  197. public function getSchema() {
  198. return $this->schema;
  199. }
  200. /**
  201. * Performs either an update or insert into the table using the values.
  202. *
  203. * If the record already exists it will be updated. If the record does not
  204. * exist it will be inserted. This function adds a bit more overhead by
  205. * checking for the existence of the record and performing the appropriate
  206. * action. You can save time by using the insert or update functions directly
  207. * if you only need to do one of those actions specifically.
  208. *
  209. * @throws Exception
  210. */
  211. public function save() {
  212. // Determine if we need to perform an update or an insert.
  213. $num_matches = $this->find();
  214. if ($num_matches == 1) {
  215. $this->update();
  216. }
  217. if ($num_matches == 0) {
  218. $this->insert();
  219. }
  220. if ($num_matches > 1) {
  221. $message = t('ChadoRecord::save(). Could not save the record into the table, !table. '.
  222. 'Multiple records already exist that match the values: !values. '.
  223. 'Please provide a set of values that can uniquely identify a record.',
  224. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  225. throw new Exception($message);
  226. }
  227. }
  228. /**
  229. * Inserts the values of this object as a new record.
  230. *
  231. * @throws Exception
  232. */
  233. public function insert() {
  234. // Make sure we have values for this record before inserting.
  235. if (empty($this->values)) {
  236. $message = t('ChadoRecord::insert(). Could not insert a record into the table, !table, without any values.',
  237. ['!table' => $this->table_name]);
  238. throw new Exception($message);
  239. }
  240. // Additionally, make sure we have all the required values!
  241. if (!empty($this->missing_required_col)) {
  242. $message = t('ChadoRecord::insert(). The columns named, "!columns", require a value for the table: "!table". You can set these values using ChadoRecord::setValues().',
  243. ['!columns' => implode('", "', $this->missing_required_col), '!table' => $this->table_name]);
  244. throw new Exception($message);
  245. }
  246. // Build the SQL statement for insertion.
  247. $insert_cols = [];
  248. $insert_vals = [];
  249. $insert_args = [];
  250. foreach ($this->values as $column => $value) {
  251. $insert_cols[] = $column;
  252. $insert_vals[] = ':' . $column;
  253. $insert_args[':' . $column] = $value;
  254. }
  255. $sql = 'INSERT INTO {' . $this->table_name . '} (' .
  256. implode(", ", $insert_cols) . ') VALUES (' .
  257. implode(", ", $insert_vals) . ')';
  258. try {
  259. chado_query($sql, $insert_args);
  260. // @todo we can speed up inserts if we can find a way to not have to
  261. // run the find(), but get the newly inserted record_id directly
  262. // from the insert command.
  263. // One option may be to use the `RETURNING [pkey]` keywords in the SQL statement.
  264. $this->find();
  265. }
  266. catch (Exception $e) {
  267. $message = t('ChadoRecord::insert(). Could not insert a record into the table, !table, with the following values: !values. ERROR: !error',
  268. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  269. throw new Exception($message);
  270. }
  271. }
  272. /**
  273. * Updates the values of this object as a new record.
  274. *
  275. * @throws Exception
  276. */
  277. public function update() {
  278. // Make sure we have values for this record before updating.
  279. if (empty($this->values)) {
  280. $message = t('ChadoRecord::update(). Could not update a record into the table, !table, without any values.',
  281. ['!table' => $this->table_name]);
  282. throw new Exception($message);
  283. }
  284. // Additionally, make sure we have all the required values!
  285. if (!empty($this->missing_required_col)) {
  286. $message = t('ChadoRecord::update(). The columns named, "!columns", require a value for the table: "!table". You can set these values using ChadoRecord::setValues().',
  287. ['!columns' => implode('", "', $this->missing_required_col), '!table' => $this->table_name]);
  288. throw new Exception($message);
  289. }
  290. // We have to have a record ID for the record to update.
  291. if (!$this->record_id) {
  292. $message = t('ChadoRecord::update(). Could not update a record in the table, !table, without a record ID.',
  293. ['!table' => $this->table_name]);
  294. throw new Exception($message);
  295. }
  296. // Build the SQL statement for updating.
  297. $update_args = [];
  298. $sql = 'UPDATE {' . $this->table_name . '} SET ';
  299. foreach ($this->values as $column => $value) {
  300. // We're not updating the primary key so skip that if it's in the values.
  301. if ($column == $this->pkey) {
  302. continue;
  303. }
  304. $sql .= $column . ' = :' . $column . ', ';
  305. $update_args[':' . $column] = $value;
  306. }
  307. // Remove the trailing comma and space.
  308. $sql = substr($sql, 0, -2);
  309. $sql .= ' WHERE ' . $this->pkey . ' = :record_id';
  310. $update_args[':record_id'] = $this->record_id;
  311. // Now try the update.
  312. try {
  313. chado_query($sql, $update_args);
  314. }
  315. catch (Exception $e) {
  316. $message = t('ChadoRecord::update(). Could not update a record in the table, !table, with !record_id as the record ID and the following values: !values. ERROR: !error',
  317. ['!table' => $this->table_name,
  318. '!record_id' => $this->record_id,
  319. '!values' => print_r($this->values, TRUE),
  320. '!error' => $e->getMessage()]);
  321. throw new Exception($message);
  322. }
  323. }
  324. /**
  325. * Deletes the record that matches the given values.
  326. *
  327. * A record ID must be part of the current values.
  328. *
  329. * @throws Exception
  330. */
  331. public function delete() {
  332. // We have to have a record ID for the record to be deleted.
  333. if (!$this->record_id) {
  334. $message = t('ChadoRecord::delete(). Could not delete a record in the table, !table, without a record ID.',
  335. ['!table' => $this->table_name]);
  336. throw new Exception($message);
  337. }
  338. try {
  339. $sql = 'DELETE FROM {' . $this->table_name . '} WHERE ' . $this->pkey . ' = :record_id';
  340. chado_query($sql, [':record_id' => $this->record_id]);
  341. }
  342. catch (Exception $e) {
  343. $message = t('ChadoRecord::delete(). Could not delete a record in the table, !table, with !record_id as the record ID. ERROR: !error',
  344. ['!table' => $this->table_name,
  345. '!record_id' => $this->record_id,
  346. '!error' => $e->getMessage()]);
  347. throw new Exception($message);
  348. }
  349. }
  350. /**
  351. * A general-purpose setter function to set the column values for the record.
  352. *
  353. * This function should be used prior to insert or update of a record. For
  354. * an update, be sure to include the record ID in the list of values passed
  355. * to the function.
  356. *
  357. * @param array $values
  358. * An associative array where the keys are the table column names and
  359. * the values are the record values for each column.
  360. *
  361. * @throws Exception
  362. */
  363. public function setValues($values) {
  364. // Intiailze the values array.
  365. $this->values = [];
  366. // Add the values provided into the values property.
  367. foreach ($values as $column => $value) {
  368. if (in_array($column, $this->column_names)) {
  369. $this->values[$column] = $value;
  370. }
  371. else {
  372. $message = t('ChadoRecord::setValues(). The column named, "!column", does not exist in table: "!table". Values: !values".',
  373. ['!column' => $column, '!table' => $this->table_name, '!values' => print_r($values, TRUE)]);
  374. throw new Exception($message);
  375. }
  376. }
  377. // Check whether all required columns are set and indicate using the
  378. // $required_values_set flag for faster checking in insert/update.
  379. $this->missing_required_col = [];
  380. foreach ($this->required_cols as $rcol) {
  381. // It's okay if the primary key is missing, esepecially if the user
  382. // wants to use the find() or insert() functions.
  383. if ($rcol == $this->pkey) {
  384. continue;
  385. }
  386. if (in_array($rcol, array_keys($this->values)) and $this->values[$rcol] === '__NULL__') {
  387. $this->missing_required_col[$rcol] = $rcol;
  388. }
  389. }
  390. // Check to see if the user provided the primary key (record_id).
  391. if (in_array($this->pkey, array_keys($values))) {
  392. $this->record_id = $values[$this->pkey];
  393. }
  394. // Ensure that no values are arrays.
  395. foreach ($values as $column => $value) {
  396. if (is_array($value)) {
  397. $message = t('ChadoRecord::setValues(). The column named, "!column", must be a single value but is currently: "!values". NOTE: we currently don\'t support expanding foreign key relationships or multiple values for a given column.',
  398. ['!column' => $column, '!table' => $this->table_name, '!values' => implode('", "', $value)]);
  399. throw new Exception($message);
  400. }
  401. }
  402. }
  403. /**
  404. * Returns all values for the record.
  405. *
  406. * @return array
  407. */
  408. public function getValues() {
  409. return $this->values;
  410. }
  411. /**
  412. * Sets the value for a specific column.
  413. *
  414. * @param string $column_name
  415. * The name of the column to which the value should be set.
  416. * @param $value
  417. * The value to set.
  418. */
  419. public function setValue($column_name, $value) {
  420. // Make sure the column is valid.
  421. if (!in_array($column_name, $this->column_names)) {
  422. $message = t('ChadoRecord::setValue(). The column named, "!column", does not exist in table: "!table".',
  423. ['!column' => $column_name, '!table' => $this->table_name]);
  424. throw new Exception($message);
  425. }
  426. // Make sure that the value is not NULL if this is a required field.
  427. if (!in_array($column_name, $this->required_cols) and $value == '__NULL__') {
  428. $message = t('ChadoRecord::setValue(). The column named, "!column", requires a value for the table: "!table".',
  429. ['!column' => $column_name, '!table' => $this->table_name]);
  430. throw new Exception($message);
  431. }
  432. $this->values[$column_name] = $value;
  433. }
  434. /**
  435. * Returns the value of a specific column.
  436. *
  437. * @param string $column_name
  438. * The name of a column from the table from which to retrieve the value.
  439. */
  440. public function getValue($column_name) {
  441. // Make sure the column is valid.
  442. if (!in_array($column_name, $this->column_names)) {
  443. $message = t('ChadoRecord::getValue(). The column named, "!column", does not exist in table: "!table".',
  444. ['!column' => $column_name, '!table' => $this->table_name]);
  445. throw new Exception($message);
  446. }
  447. return $this->values[$column_name];
  448. }
  449. /**
  450. * Uses the current values given to this object to find a record.
  451. *
  452. * Use the setValues function first to set values for searching, then call
  453. * this function to find matching record. The values provided to the
  454. * setValues function must uniquely identify a record.
  455. *
  456. * @return
  457. * The number of matches found. If 1 is returned then the query
  458. * successfully found a match. If 0 then no matching records were found.
  459. *
  460. * @throws Exception
  461. */
  462. public function find() {
  463. // Make sure we have values for this record before searching.
  464. if (empty($this->values)) {
  465. $message = t('ChadoRecord::find(). Could not find a record from the table, !table, without any values.',
  466. ['!table' => $this->table_name]);
  467. throw new Exception($message);
  468. }
  469. // Build the SQL statement for searching.
  470. $select_args = [];
  471. $sql = 'SELECT * FROM {' . $this->table_name . '} WHERE 1=1 ';
  472. foreach ($this->values as $column => $value) {
  473. $sql .= ' AND ' . $column . ' = :' . $column;
  474. $select_args[':' . $column] = $value;
  475. }
  476. try {
  477. $results = chado_query($sql, $select_args);
  478. }
  479. catch (Exception $e) {
  480. $message = t('ChadoRecord::find(). Could not find a record in the table, !table, with the following values: !values. ERROR: !error',
  481. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  482. throw new Exception($message);
  483. }
  484. // If we only have a single match then we're good and we can update the
  485. // values for this object.
  486. $num_matches = $results->rowCount();
  487. if ($num_matches == 1) {
  488. $record = $results->fetchAssoc();
  489. $this->values = [];
  490. foreach ($record as $column => $value) {
  491. $this->values[$column] = $value;
  492. }
  493. $this->record_id = $record[$this->pkey];
  494. }
  495. // Return the number of matches.
  496. return $num_matches;
  497. }
  498. }