ChadoRecord.inc 22 KB

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