ChadoRecord.inc 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  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. * @todo Support options from chado_insert_record: return_record.
  232. * @todo check for violation of unique constraint.
  233. *
  234. * @throws Exception
  235. */
  236. public function insert() {
  237. // Make sure we have values for this record before inserting.
  238. if (empty($this->values)) {
  239. $message = t('ChadoRecord::insert(). Could not insert a record into the table, !table, without any values.',
  240. ['!table' => $this->table_name]);
  241. throw new Exception($message);
  242. }
  243. // Additionally, make sure we have all the required values!
  244. if (!empty($this->missing_required_col)) {
  245. $message = t('ChadoRecord::insert(). The columns named, "!columns", require a value for the table: "!table". You can set these values using ChadoRecord::setValues().',
  246. ['!columns' => implode('", "', $this->missing_required_col), '!table' => $this->table_name]);
  247. throw new Exception($message);
  248. }
  249. // Build the SQL statement for insertion.
  250. $insert_cols = [];
  251. $insert_vals = [];
  252. $insert_args = [];
  253. foreach ($this->values as $column => $value) {
  254. $insert_cols[] = $column;
  255. $insert_vals[] = ':' . $column;
  256. $insert_args[':' . $column] = $value;
  257. }
  258. $sql = 'INSERT INTO {' . $this->table_name . '} (' .
  259. implode(", ", $insert_cols) . ') VALUES (' .
  260. implode(", ", $insert_vals) . ')';
  261. try {
  262. chado_query($sql, $insert_args);
  263. // @todo we can speed up inserts if we can find a way to not have to
  264. // run the find(), but get the newly inserted record_id directly
  265. // from the insert command.
  266. // One option may be to use the `RETURNING [pkey]` keywords in the SQL statement.
  267. $this->find();
  268. }
  269. catch (Exception $e) {
  270. $message = t('ChadoRecord::insert(). Could not insert a record into the table, !table, with the following values: !values. ERROR: !error',
  271. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  272. throw new Exception($message);
  273. }
  274. }
  275. /**
  276. * Updates the values of this object as a new record.
  277. *
  278. * @todo set defaults for columns not already set in values.
  279. * @todo Support options from chado_update_record: return_record.
  280. * @todo check for violation of unique constraint.
  281. * @todo if record_id not set then try finding it.
  282. *
  283. * @throws Exception
  284. */
  285. public function update() {
  286. // Make sure we have values for this record before updating.
  287. if (empty($this->values)) {
  288. $message = t('ChadoRecord::update(). Could not update a record into the table, !table, without any values.',
  289. ['!table' => $this->table_name]);
  290. throw new Exception($message);
  291. }
  292. // Additionally, make sure we have all the required values!
  293. if (!empty($this->missing_required_col)) {
  294. $message = t('ChadoRecord::update(). The columns named, "!columns", require a value for the table: "!table". You can set these values using ChadoRecord::setValues().',
  295. ['!columns' => implode('", "', $this->missing_required_col), '!table' => $this->table_name]);
  296. throw new Exception($message);
  297. }
  298. // We have to have a record ID for the record to update.
  299. if (!$this->record_id) {
  300. $message = t('ChadoRecord::update(). Could not update a record in the table, !table, without a record ID.',
  301. ['!table' => $this->table_name]);
  302. throw new Exception($message);
  303. }
  304. // Build the SQL statement for updating.
  305. $update_args = [];
  306. $sql = 'UPDATE {' . $this->table_name . '} SET ';
  307. foreach ($this->values as $column => $value) {
  308. // We're not updating the primary key so skip that if it's in the values.
  309. if ($column == $this->pkey) {
  310. continue;
  311. }
  312. $sql .= $column . ' = :' . $column . ', ';
  313. $update_args[':' . $column] = $value;
  314. }
  315. // Remove the trailing comma and space.
  316. $sql = substr($sql, 0, -2);
  317. $sql .= ' WHERE ' . $this->pkey . ' = :record_id';
  318. $update_args[':record_id'] = $this->record_id;
  319. // Now try the update.
  320. try {
  321. chado_query($sql, $update_args);
  322. }
  323. catch (Exception $e) {
  324. $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',
  325. ['!table' => $this->table_name,
  326. '!record_id' => $this->record_id,
  327. '!values' => print_r($this->values, TRUE),
  328. '!error' => $e->getMessage()]);
  329. throw new Exception($message);
  330. }
  331. }
  332. /**
  333. * Deletes the record that matches the given values.
  334. *
  335. * A record ID must be part of the current values.
  336. *
  337. * @throws Exception
  338. */
  339. public function delete() {
  340. // We have to have a record ID for the record to be deleted.
  341. if (!$this->record_id) {
  342. $message = t('ChadoRecord::delete(). Could not delete a record in the table, !table, without a record ID.',
  343. ['!table' => $this->table_name]);
  344. throw new Exception($message);
  345. }
  346. try {
  347. $sql = 'DELETE FROM {' . $this->table_name . '} WHERE ' . $this->pkey . ' = :record_id';
  348. chado_query($sql, [':record_id' => $this->record_id]);
  349. }
  350. catch (Exception $e) {
  351. $message = t('ChadoRecord::delete(). Could not delete a record in the table, !table, with !record_id as the record ID. ERROR: !error',
  352. ['!table' => $this->table_name,
  353. '!record_id' => $this->record_id,
  354. '!error' => $e->getMessage()]);
  355. throw new Exception($message);
  356. }
  357. }
  358. /**
  359. * A general-purpose setter function to set the column values for the record.
  360. *
  361. * This function should be used prior to insert or update of a record. For
  362. * an update, be sure to include the record ID in the list of values passed
  363. * to the function.
  364. *
  365. * @todo Support options from chado_insert_record: skip_validation.
  366. * @todo Validate the types match what is expected based on the schema.
  367. * @todo Set default values for columns not in this array?
  368. * @todo Support foreign key relationships: lookup the key.
  369. * @todo Support value = [a, b, c] for IN select statements?
  370. *
  371. * @param array $values
  372. * An associative array where the keys are the table column names and
  373. * the values are the record values for each column.
  374. *
  375. * @throws Exception
  376. */
  377. public function setValues($values) {
  378. // Intiailze the values array.
  379. $this->values = [];
  380. // Add the values provided into the values property.
  381. foreach ($values as $column => $value) {
  382. if (in_array($column, $this->column_names)) {
  383. $this->values[$column] = $value;
  384. }
  385. else {
  386. $message = t('ChadoRecord::setValues(). The column named, "!column", does not exist in table: "!table". Values: !values".',
  387. ['!column' => $column, '!table' => $this->table_name, '!values' => print_r($values, TRUE)]);
  388. throw new Exception($message);
  389. }
  390. }
  391. // Check whether all required columns are set and indicate using the
  392. // $required_values_set flag for faster checking in insert/update.
  393. $this->missing_required_col = [];
  394. foreach ($this->required_cols as $rcol) {
  395. // It's okay if the primary key is missing, esepecially if the user
  396. // wants to use the find() or insert() functions.
  397. if ($rcol == $this->pkey) {
  398. continue;
  399. }
  400. if (in_array($rcol, array_keys($this->values)) and $this->values[$rcol] === '__NULL__') {
  401. $this->missing_required_col[$rcol] = $rcol;
  402. }
  403. }
  404. // Check to see if the user provided the primary key (record_id).
  405. if (in_array($this->pkey, array_keys($values))) {
  406. $this->record_id = $values[$this->pkey];
  407. }
  408. // Ensure that no values are arrays.
  409. foreach ($values as $column => $value) {
  410. if (is_array($value)) {
  411. $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.',
  412. ['!column' => $column, '!table' => $this->table_name, '!values' => implode('", "', $value)]);
  413. throw new Exception($message);
  414. }
  415. }
  416. }
  417. /**
  418. * Returns all values for the record.
  419. *
  420. * @todo We need to follow foreign key constraints.
  421. *
  422. * @return array
  423. */
  424. public function getValues() {
  425. return $this->values;
  426. }
  427. /**
  428. * Sets the value for a specific column.
  429. *
  430. * @todo Support options from chado_insert_record: skip_validation.
  431. * @todo Validate the types match what is expected based on the schema.
  432. * @todo Set default values for columns not in this array?
  433. * @todo Support foreign key relationships: lookup the key.
  434. * @todo Support value = [a, b, c] for IN select statements?
  435. *
  436. * @param string $column_name
  437. * The name of the column to which the value should be set.
  438. * @param $value
  439. * The value to set.
  440. */
  441. public function setValue($column_name, $value) {
  442. // Make sure the column is valid.
  443. if (!in_array($column_name, $this->column_names)) {
  444. $message = t('ChadoRecord::setValue(). The column named, "!column", does not exist in table: "!table".',
  445. ['!column' => $column_name, '!table' => $this->table_name]);
  446. throw new Exception($message);
  447. }
  448. // Make sure that the value is not NULL if this is a required field.
  449. if (!in_array($column_name, $this->required_cols) and $value == '__NULL__') {
  450. $message = t('ChadoRecord::setValue(). The column named, "!column", requires a value for the table: "!table".',
  451. ['!column' => $column_name, '!table' => $this->table_name]);
  452. throw new Exception($message);
  453. }
  454. // Remove from the missing list if it was there.
  455. elseif (isset($this->missing_required_cols[$column])) {
  456. unset($this->missing_required_cols[$column]);
  457. }
  458. // Ensure that no values are arrays.
  459. if (is_array($value)) {
  460. $message = t('ChadoRecord::setValue(). 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.',
  461. ['!column' => $column, '!table' => $this->table_name, '!values' => implode('", "', $value)]);
  462. throw new Exception($message);
  463. }
  464. $this->values[$column_name] = $value;
  465. }
  466. /**
  467. * Returns the value of a specific column.
  468. *
  469. * @param string $column_name
  470. * The name of a column from the table from which to retrieve the value.
  471. */
  472. public function getValue($column_name) {
  473. // Make sure the column is valid.
  474. if (!in_array($column_name, $this->column_names)) {
  475. $message = t('ChadoRecord::getValue(). The column named, "!column", does not exist in table: "!table".',
  476. ['!column' => $column_name, '!table' => $this->table_name]);
  477. throw new Exception($message);
  478. }
  479. return $this->values[$column_name];
  480. }
  481. /**
  482. * Uses the current values given to this object to find a record.
  483. *
  484. * Use the setValues function first to set values for searching, then call
  485. * this function to find matching record. The values provided to the
  486. * setValues function must uniquely identify a record.
  487. *
  488. * @todo Support options from chado_select_record: skip_validation, has_record,
  489. * return_sql, case_insensitive_columns, regex_columns, order_by, is_duplicate,
  490. * pager, limit, offset.
  491. * @todo Support following the foreign key
  492. * @todo Support complex filtering (e.g. fmin > 50)
  493. * @todo Support multiple records being returned?
  494. *
  495. * @return
  496. * The number of matches found. If 1 is returned then the query
  497. * successfully found a match. If 0 then no matching records were found.
  498. *
  499. * @throws Exception
  500. */
  501. public function find() {
  502. // Make sure we have values for this record before searching.
  503. if (empty($this->values)) {
  504. $message = t('ChadoRecord::find(). Could not find a record from the table, !table, without any values.',
  505. ['!table' => $this->table_name]);
  506. throw new Exception($message);
  507. }
  508. // Build the SQL statement for searching.
  509. $select_args = [];
  510. $sql = 'SELECT * FROM {' . $this->table_name . '} WHERE 1=1 ';
  511. foreach ($this->values as $column => $value) {
  512. $sql .= ' AND ' . $column . ' = :' . $column;
  513. $select_args[':' . $column] = $value;
  514. }
  515. try {
  516. $results = chado_query($sql, $select_args);
  517. }
  518. catch (Exception $e) {
  519. $message = t('ChadoRecord::find(). Could not find a record in the table, !table, with the following values: !values. ERROR: !error',
  520. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  521. throw new Exception($message);
  522. }
  523. // If we only have a single match then we're good and we can update the
  524. // values for this object.
  525. $num_matches = $results->rowCount();
  526. if ($num_matches == 1) {
  527. $record = $results->fetchAssoc();
  528. $this->values = [];
  529. foreach ($record as $column => $value) {
  530. $this->values[$column] = $value;
  531. }
  532. $this->record_id = $record[$this->pkey];
  533. }
  534. // Return the number of matches.
  535. return $num_matches;
  536. }
  537. }