ChadoRecord.inc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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 integer
  96. * The numeric Id for this record.
  97. */
  98. protected $record_id = NULL;
  99. /**
  100. * @var string
  101. * The column name for the primary key.
  102. */
  103. protected $pkey = '';
  104. /**
  105. * The list of column names in the table.
  106. * @var array
  107. */
  108. protected $column_names = [];
  109. /**
  110. * The ChadoRecord constructor
  111. *
  112. * @param string $table_name
  113. * The name of the table that the record belongs to.
  114. *
  115. * @param string $record_id
  116. * An optional record ID if this record is already present in Chado.
  117. */
  118. public function __construct($table_name, $record_id = NULL) {
  119. if (!$table_name) {
  120. $message = t('ChadoRecord::_construct(). The $table_name argument is required for a ChadoRecord instance.');
  121. throw new Exception($message);
  122. }
  123. // Set the table name and schema.
  124. $this->table_name = $table_name;
  125. $this->schema = chado_get_schema($this->table_name);
  126. if (!$this->schema) {
  127. $message = t('ChadoRecord::_construct(). Could not find a matching table schema in Chado for the table: !table.',
  128. ['!table' => $this->table_name]);
  129. throw new Exception($message);
  130. }
  131. // Chado tables never have more than one column as a primary key so
  132. // we are good just getting the first element.
  133. $this->pkey = $this->schema['primary key'][0];
  134. // Save the column names.
  135. foreach ($this->schema['fields'] as $column_name => $col_details) {
  136. $this->column_names[] = $column_name;
  137. }
  138. // Get the required columns.
  139. foreach ($this->schema['fields'] as $column => $col_schema) {
  140. foreach ($col_schema as $param => $val) {
  141. if (preg_match('/not null/i', $param) and $col_schema[$param]) {
  142. $this->required_cols[] = $column;
  143. }
  144. }
  145. }
  146. // If a record_id was provided then lookup the record and set the values.
  147. if ($record_id) {
  148. try {
  149. $sql = 'SELECT * FROM {' . $this->table_name . '} WHERE ' . $this->pkey . ' = :record_id';
  150. $result = chado_query($sql, [':record_id' => $record_id]);
  151. $values = $result->fetchAssoc();
  152. if (empty($values)) {
  153. $message = t('ChadoRecord::_construct(). Could not find a record in table, !table, with the given !pkey: !record_id.',
  154. ['!pkey' => $this->pkey, '!record_id' => $record_id, '!table' => $this->table_name]);
  155. throw new Exception($message);
  156. }
  157. $this->record_id = $record_id;
  158. $this->values = $values;
  159. }
  160. catch (Exception $e) {
  161. $message = t('ChadoRecord::_construct(). Could not find a record in table, !table, with the given !pkey: !record_id. ERROR: !error',
  162. ['!pkey' => $this->pkey, '!record_id' => $record_id, '!table' => $this->table_name, '!error' => $e->getMessage()]);
  163. throw new Exception($message);
  164. }
  165. }
  166. }
  167. /**
  168. * Retrieves the record ID.
  169. *
  170. * @return number
  171. */
  172. public function getID() {
  173. return $this->record_id;
  174. }
  175. /**
  176. * Retrieves the table name.
  177. *
  178. * @return string
  179. * The name of the table that the record belongs to.
  180. */
  181. public function getTable() {
  182. return $this->table_name;
  183. }
  184. /**
  185. * Retrieves the table schema.
  186. *
  187. * @return array
  188. * The Drupal schema array for the table.
  189. */
  190. public function getSchema() {
  191. return $this->schema;
  192. }
  193. /**
  194. * Performs either an update or insert into the table using the values.
  195. *
  196. * If the record already exists it will be updated. If the record does not
  197. * exist it will be inserted. This function adds a bit more overhead by
  198. * checking for the existence of the record and performing the appropriate
  199. * action. You can save time by using the insert or update functions directly
  200. * if you only need to do one of those actions specifically.
  201. *
  202. * @throws Exception
  203. */
  204. public function save() {
  205. // Determine if we need to perform an update or an insert.
  206. $num_matches = $this->find();
  207. if ($num_matches == 1) {
  208. $this->update();
  209. }
  210. if ($num_matches == 0) {
  211. $this->insert();
  212. }
  213. if ($num_matches > 1) {
  214. $message = t('ChadoRecord::save(). Could not save the record into the table, !table. '.
  215. 'Multiple records already exist that match the values: !values. '.
  216. 'Please provide a set of values that can uniquely identify a record.',
  217. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  218. throw new Exception($message);
  219. }
  220. }
  221. /**
  222. * Inserts the values of this object as a new record.
  223. *
  224. * @throws Exception
  225. */
  226. public function insert() {
  227. // Make sure we have values for this record before inserting.
  228. if (empty($this->values)) {
  229. $message = t('ChadoRecord::insert(). Could not insert a record into the table, !table, without any values.',
  230. ['!table' => $this->table_name]);
  231. throw new Exception($message);
  232. }
  233. // Build the SQL statement for insertion.
  234. $insert_cols = [];
  235. $insert_vals = [];
  236. $insert_args = [];
  237. foreach ($this->values as $column => $value) {
  238. $insert_cols[] = $column;
  239. $insert_vals[] = ':' . $column;
  240. $insert_args[':' . $column] = $value;
  241. }
  242. $sql = 'INSERT INTO {' . $this->table_name . '} (' .
  243. implode(", ", $insert_cols) . ') VALUES (' .
  244. implode(", ", $insert_vals) . ')';
  245. try {
  246. chado_query($sql, $insert_args);
  247. // TODO: we can speed up inserts if we can find a way to not have to
  248. // run the find(), but get the newly inserted record_id directly
  249. // from the insert command.
  250. $this->find();
  251. }
  252. catch (Exception $e) {
  253. $message = t('ChadoRecord::insert(). Could not insert a record into the table, !table, with the following values: !values. ERROR: !error',
  254. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  255. throw new Exception($message);
  256. }
  257. }
  258. /**
  259. * Updates the values of this object as a new record.
  260. *
  261. * @throws Exception
  262. */
  263. public function update() {
  264. // Make sure we have values for this record before updating.
  265. if (empty($this->values)) {
  266. $message = t('ChadoRecord::update(). Could not update a record into the table, !table, without any values.',
  267. ['!table' => $this->table_name]);
  268. throw new Exception($message);
  269. }
  270. // We have to have a record ID for the record to update.
  271. if (!$this->record_id) {
  272. $message = t('ChadoRecord::update(). Could not update a record in the table, !table, without a record ID.',
  273. ['!table' => $this->table_name]);
  274. throw new Exception($message);
  275. }
  276. // Build the SQL statement for updating.
  277. $update_args = [];
  278. $sql = 'UPDATE {' . $this->table_name . '} SET ';
  279. foreach ($this->values as $column => $value) {
  280. // We're not updating the primary key so skip that if it's in the values.
  281. if ($column == $this->pkey) {
  282. continue;
  283. }
  284. $sql .= $column . ' = :' . $column . ', ';
  285. $update_args[':' . $column] = $value;
  286. }
  287. // Remove the trailing comma and space.
  288. $sql = substr($sql, 0, -2);
  289. $sql .= ' WHERE ' . $this->pkey . ' = :record_id';
  290. $update_args[':record_id'] = $this->record_id;
  291. // Now try the update.
  292. try {
  293. chado_query($sql, $update_args);
  294. }
  295. catch (Exception $e) {
  296. $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',
  297. ['!table' => $this->table_name,
  298. '!record_id' => $this->record_id,
  299. '!values' => print_r($this->values, TRUE),
  300. '!error' => $e->getMessage()]);
  301. throw new Exception($message);
  302. }
  303. }
  304. /**
  305. * Deletes the record that matches the given values.
  306. *
  307. * A record ID must be part of the current values.
  308. *
  309. * @throws Exception
  310. */
  311. public function delete() {
  312. // We have to have a record ID for the record to be deleted.
  313. if (!$this->record_id) {
  314. $message = t('ChadoRecord::delete(). Could not delete a record in the table, !table, without a record ID.',
  315. ['!table' => $this->table_name]);
  316. throw new Exception($message);
  317. }
  318. try {
  319. $sql = 'DELETE FROM {' . $this->table_name . '} WHERE ' . $this->pkey . ' = :record_id';
  320. chado_query($sql, [':record_id' => $this->record_id]);
  321. }
  322. catch (Exception $e) {
  323. $message = t('ChadoRecord::delete(). Could not delete a record in the table, !table, with !record_id as the record ID. ERROR: !error',
  324. ['!table' => $this->table_name,
  325. '!record_id' => $this->record_id,
  326. '!error' => $e->getMessage()]);
  327. throw new Exception($message);
  328. }
  329. }
  330. /**
  331. * A general-purpose setter function to set the column values for the record.
  332. *
  333. * This function should be used prior to insert or update of a record. For
  334. * an update, be sure to include the record ID in the list of values passed
  335. * to the function.
  336. *
  337. * @param array $values
  338. * An associative array where the keys are the table column names and
  339. * the values are the record values for each column.
  340. *
  341. * @throws Exception
  342. */
  343. public function setValues($values) {
  344. // Intiailze the values array.
  345. $this->values = [];
  346. // Add the values provided into the values property.
  347. foreach ($values as $column => $value) {
  348. if (in_array($column, $this->column_names)) {
  349. $this->values[$column] = $value;
  350. }
  351. else {
  352. $message = t('ChadoRecord::setValues(). The column named, "!column", does not exist in table: "!table". Values: !values".',
  353. ['!column' => $column, '!table' => $this->table_name, '!values' => print_r($values, TRUE)]);
  354. throw new Exception($message);
  355. }
  356. }
  357. // Make sure that the user did not miss any required columns or has
  358. // set a column to be NULL when it doesn't allow NULLs.
  359. foreach ($this->required_cols as $rcol) {
  360. // It's okay if the primary key is missing, esepecially if the user
  361. // wants to use the find() or insert() functions.
  362. if ($rcol == $this->pkey) {
  363. continue;
  364. }
  365. if (in_array($rcol, array_keys($this->values)) and $this->values[$rcol] === '__NULL__') {
  366. $message = t('ChadoRecord::setValues(). The column named, "!column", requires a value for the table: "!table".',
  367. ['!column' => $rcol, '!table' => $this->table_name]);
  368. throw new Exception($message);
  369. }
  370. }
  371. // Check to see if the user provided the primary key (record_id).
  372. if (in_array($this->pkey, array_keys($values))) {
  373. $this->record_id = $values[$this->pkey];
  374. }
  375. }
  376. /**
  377. * Returns all values for the record.
  378. *
  379. * @return array
  380. */
  381. public function getValues() {
  382. return $this->values;
  383. }
  384. /**
  385. * Sets the value for a specific column.
  386. *
  387. * @param string $column_name
  388. * The name of the column to which the value should be set.
  389. * @param $value
  390. * The value to set.
  391. */
  392. public function setValue($column_name, $value) {
  393. // Make sure the column is valid.
  394. if (!in_array($column_name, $this->column_names)) {
  395. $message = t('ChadoRecord::setValue(). The column named, "!column", does not exist in table: "!table".',
  396. ['!column' => $column_name, '!table' => $this->table_name]);
  397. throw new Exception($message);
  398. }
  399. // Make sure that the value is not NULL if this is a required field.
  400. if (!in_array($column_name, $this->required_cols) and $value == '__NULL__') {
  401. $message = t('ChadoRecord::setValue(). The column named, "!column", requires a value for the table: "!table".',
  402. ['!column' => $column_name, '!table' => $this->table_name]);
  403. throw new Exception($message);
  404. }
  405. $this->values[$column_name] = $value;
  406. }
  407. /**
  408. * Returns the value of a specific column.
  409. *
  410. * @param string $column_name
  411. * The name of a column from the table from which to retrieve the value.
  412. */
  413. public function getValue($column_name) {
  414. // Make sure the column is valid.
  415. if (!in_array($column_name, $this->column_names)) {
  416. $message = t('ChadoRecord::getValue(). The column named, "!column", does not exist in table: "!table".',
  417. ['!column' => $column_name, '!table' => $this->table_name]);
  418. throw new Exception($message);
  419. }
  420. return $this->values[$column_name];
  421. }
  422. /**
  423. * Uses the current values given to this object to find a record.
  424. *
  425. * Use the setValues function first to set values for searching, then call
  426. * this function to find matching record. The values provided to the
  427. * setValues function must uniquely identify a record.
  428. *
  429. * @return
  430. * The number of matches found. If 1 is returned then the query
  431. * successfully found a match. If 0 then no matching records were found.
  432. *
  433. * @throws Exception
  434. */
  435. public function find() {
  436. // Make sure we have values for this record before searching.
  437. if (empty($this->values)) {
  438. $message = t('ChadoRecord::find(). Could not find a record from the table, !table, without any values.',
  439. ['!table' => $this->table_name]);
  440. throw new Exception($message);
  441. }
  442. // Build the SQL statement for searching.
  443. $select_args = [];
  444. $sql = 'SELECT * FROM {' . $this->table_name . '} WHERE 1=1 ';
  445. foreach ($this->values as $column => $value) {
  446. $sql .= ' AND ' . $column . ' = :' . $column;
  447. $select_args[':' . $column] = $value;
  448. }
  449. try {
  450. $results = chado_query($sql, $select_args);
  451. }
  452. catch (Exception $e) {
  453. $message = t('ChadoRecord::find(). Could not find a record in the table, !table, with the following values: !values. ERROR: !error',
  454. ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
  455. throw new Exception($message);
  456. }
  457. // If we only have a single match then we're good and we can update the
  458. // values for this object.
  459. $num_matches = $results->rowCount();
  460. if ($num_matches == 1) {
  461. $record = $results->fetchAssoc();
  462. $this->values = [];
  463. foreach ($record as $column => $value) {
  464. $this->values[$column] = $value;
  465. }
  466. $this->record_id = $record[$this->pkey];
  467. }
  468. // Return the number of matches.
  469. return $num_matches;
  470. }
  471. }