labdb.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. classdef (Sealed) labdb < handle
  2. % Class labdb
  3. % This is a wrapper class for the JDBC MySQL driver.
  4. % It has several features which are generally useful
  5. % 1. It reads credentials configurations from ~/.dbconf so that you don't have to type in credentials or store them in code.
  6. % 2. It maintains a single connection per configuration (a configuration is a user/hostname pair) for memory efficiency.
  7. % 3. It has several useful functions for getting and saving data to MySQL
  8. % 4. By default, automatically checks that the connection is alive and well before trying to communicate with the database.
  9. % This incurs a small overhead, and can be turned off with `obj.autocheck = false`
  10. %
  11. % To see a list of the functions for the class type db.labdb.help
  12. % To get help for a specific function call db.labdb.help(function_name), e.g. db.labdb.help('query')
  13. properties (SetAccess=public,GetAccess=protected)
  14. config = [];
  15. end
  16. properties (SetAccess=public,GetAccess=public)
  17. dbconn = [];
  18. autocheck = true;% Normally this class checks for DB connectivity before queries. If you are running many you can skip the autocheck.
  19. end
  20. methods (Access=private)
  21. function obj = labdb
  22. end
  23. end
  24. methods (Access=public)
  25. function setConfig(obj, config)
  26. % Manually set the user, passwd, host.
  27. % input should be a struct with those fields
  28. obj.config = config;
  29. end
  30. function host = getHost(obj)
  31. % host = getHost()
  32. % output: the hostname from the current config
  33. host = obj.config.host;
  34. end
  35. function user = getUser(obj)
  36. % host = getUser()
  37. % output: the user from the current config
  38. user = obj.config.user;
  39. end
  40. function out = getConnectionInfo(obj)
  41. % out = getConnectionInfo
  42. % returns a struct with connection information (driver version, connection URL, etc)
  43. out = ping(obj.dbconn);
  44. end
  45. function list = list_enums(obj, tablename, column)
  46. out = obj.query('show columns from %s where field="%s"',{tablename, column});
  47. enums = out.COLUMN_TYPE{1}(6:end-1);
  48. done = false;
  49. list = {};
  50. while ~done
  51. [this_one, enums] = strtok(enums, ',');
  52. list = [list, {strtrim(replace(this_one,'''',''))}];
  53. if isempty(enums)
  54. done = true;
  55. else
  56. enums = enums(2:end);
  57. end
  58. end
  59. end
  60. function cur = execute(obj, sqlstr, args)
  61. % cur = execute(sql_command, [input arguments])
  62. % executes the sql_command and returns a cursor object.
  63. % Place holders can be used using sprintf style syntax.
  64. % e.g. execute('insert into foo (a,b) values (%.3f,"%s")',{3.1441241, 'some text goes here'})
  65. if nargin<3
  66. args = {};
  67. end
  68. if obj.autocheck
  69. checkConnection(obj);
  70. end
  71. sqlquery = sprintf(sqlstr, args{:});
  72. cur = exec(obj.dbconn, sqlquery);
  73. if cur.Message
  74. % There was an error
  75. fprintf(2,'SQL ERROR: %s \n',cur.Message);
  76. end
  77. end
  78. function use(obj, schema)
  79. % use(schema)
  80. % sets the default schema
  81. cur = execute(obj,sprintf('use %s', schema));
  82. if cur.Message
  83. error('Failed to switch schemas')
  84. end
  85. end
  86. function out = explain(obj, table)
  87. % explain(something)
  88. % a shortcut for 'explain ...'
  89. out = query(obj,sprintf('explain %s', table));
  90. end
  91. function out = last_insert_id(obj)
  92. % out = last_insert_id
  93. % returns the last_insert_id
  94. out = query(obj,'select last_insert_id() as id');
  95. out = out.id;
  96. end
  97. function varargout = get(obj, sqlstr, args)
  98. % varargout = get(sql_command, [input arguments])
  99. % Like query, this command uses sprintf style parsing to execute a MySQL SELECT command.
  100. % However, get is special in that it returns one variable for each column in the SELECT
  101. % whereas query returns a single table for the entire query.
  102. %
  103. % e.g. sessid = obj.get('select sessid from sessions limit 1') % sessid will be a float
  104. % [sessdate, sessid] = obj.get('select sessiondate, sessid from sessions') % sessdate will be a cell array and sessid will be a vector of float
  105. if nargin < 3
  106. args = {};
  107. end
  108. out = query(obj,sqlstr,args);
  109. varargout = cell(1,nargout);
  110. if isempty(out)
  111. return;
  112. end
  113. for vx = 1:nargout
  114. varargout{vx} = out.(out.Properties.VariableNames{vx});
  115. end
  116. end
  117. function call(obj, sqlstr, args)
  118. % call('storedProcedure(2456)')
  119. % call('storedProcedure(%d,"%s")',{1234,'stuff'})
  120. % Calls the stored procedure with the passed arguments.
  121. if nargin<3
  122. args={};
  123. end
  124. execute(obj,sprintf('call %s', sprintf(sqlstr, args{:})));
  125. end
  126. function out = query(obj, sqlstr, args)
  127. % tableout = query(sql_command, [input arguments])
  128. % Like execute, this command uses sprintf style parsing. But instead of returning a cursor,
  129. % query returns a `table` object.
  130. %
  131. % e.g. sessid = obj.query('select sessid from sessions limit 1') % sessid will be a table with a sessid column.
  132. % [sessdate, sessid] = obj.get('select sessiondate, sessid from sessions') % sessdate will be a cell array and sessid will be a vector of float
  133. if obj.autocheck
  134. checkConnection(obj);
  135. end
  136. if nargin < 3
  137. args = {};
  138. end
  139. sqlquery = sprintf(sqlstr, args{:});
  140. cur = exec(obj.dbconn, sqlquery);
  141. if cur.Message
  142. % There was an error
  143. fprintf(2,'SQL ERROR: %s \n',cur.Message);
  144. out = [];
  145. else
  146. data = fetch(cur);
  147. if cur.rows <= 0
  148. out = {};
  149. else
  150. out = data.Data;
  151. end
  152. end
  153. close(cur);
  154. end
  155. function saveData(obj, tablename, data, varargin)
  156. % saveData(obj, tablename, data, colnames)
  157. if obj.autocheck
  158. checkConnection(obj);
  159. end
  160. if nargin < 4
  161. if isstruct(data)
  162. colnames = fields(data);
  163. data = struct2table(data,'AsArray',true);
  164. elseif istable(data)
  165. colnames = data.Properties.VariableNames;
  166. else
  167. error('labdb:saveData','Must specify column names if not using table or struct type')
  168. end
  169. end
  170. datainsert(obj.dbconn, tablename, colnames, data);
  171. end
  172. function ok = isopen(obj)
  173. try
  174. cur = obj.dbconn.exec('select 1 from dual');
  175. assert(isempty(cur.Message));
  176. ok = true;
  177. catch me
  178. ok = false;
  179. end
  180. end
  181. function checkConnection(obj)
  182. try
  183. getId(obj.dbconn.Handle);
  184. cur = obj.dbconn.exec('select 1 from dual');
  185. assert(isempty(cur.Message));
  186. catch
  187. obj.dbconn = [];
  188. end
  189. if isempty(obj.dbconn) || ~obj.dbconn.isopen
  190. obj.dbconn = database(obj.config.db,obj.config.user,obj.config.passwd,'Vendor','MySQL',...
  191. 'Server',obj.config.host,'PortNumber',obj.config.port);
  192. end
  193. if ~isempty(obj.dbconn.Message)
  194. fprintf(2,'%s\n',obj.dbconn.Message);
  195. obj.dbconn = [];
  196. end
  197. end
  198. function close(obj)
  199. close(obj.dbconn);
  200. obj.dbconn = [];
  201. end
  202. end
  203. methods (Static)
  204. % We use a static method to give the matlab client a database object
  205. % for each configuration (IP, username) we only make one connection and then re-use it.
  206. % This is ok for MATLAB since it is single threaded.
  207. % It could potentially cause strange behavior if a user was doing inserts in a timer and also in the main
  208. % thread and using `last_insert_id`
  209. function help(fname)
  210. if nargin == 0
  211. help('db.labdb')
  212. methods('db.labdb')
  213. else
  214. help(sprintf('db.labdb.%s',fname))
  215. end
  216. end
  217. function so = getConnection(varargin)
  218. setdbprefs('DataReturnFormat','table')
  219. addMysqlConnecterToPath(); % Make sure the driver is on the path.
  220. persistent localObj; % This is where we store existing connections.
  221. if nargin == 1
  222. % The user provided a config name, so use that.
  223. configsec = varargin{1};
  224. elseif nargin ==0
  225. % The user provided nothing. Use the default config.
  226. configsec = 'client';
  227. else
  228. configsec = utils.inputordefault('config','client',varargin);
  229. end
  230. % Check if we have a connection with the right name.
  231. try
  232. so = localObj.(configsec);
  233. return;
  234. catch ME
  235. if ~strcmp(ME.identifier, {'MATLAB:nonExistentField', 'MATLAB:structRefFromNonStruct'})
  236. rethrow(ME)
  237. end
  238. end
  239. % No connection exists
  240. localObj.(configsec) = [];
  241. if nargin < 3
  242. config = readDBconf(configsec);
  243. else
  244. config.host = varargin{1};
  245. config.user = varargin{2};
  246. config.passwd = varargin{3};
  247. if nargin > 3
  248. config.db = varargin{4};
  249. else
  250. config.db = 'met';
  251. end
  252. end
  253. localObj.(configsec) = db.labdb;
  254. setConfig(localObj.(configsec), config);
  255. checkConnection(localObj.(configsec));
  256. so = localObj.(configsec);
  257. end
  258. end
  259. end
  260. function cfg = readDBconf(cfgname)
  261. % A private function to help the labdb class read credentials from the
  262. % .dbconf file in the user's home directory.
  263. def.db = ''; % In case db is not passed in set it by default to nothing.
  264. def.port = 3306;
  265. if nargin == 0
  266. cfgname = 'client';
  267. end
  268. cfgfile = '~/.dbconf';
  269. if ~exist(cfgfile,'file')
  270. try
  271. cfgfile = fullfile(getenv('USERPROFILE'),'.dbconf');
  272. catch
  273. error('labdb:dbconf','.dbconf file not found in home directory');
  274. end
  275. end
  276. allcfg = utils.ini2struct(cfgfile);
  277. fopts = allcfg.(cfgname);
  278. cfg = utils.apply_struct(def, fopts);
  279. end
  280. function addMysqlConnecterToPath()
  281. jcp = javaclasspath('-all');
  282. jarfile = 'mysql-connector-java-5.1.42-bin.jar';
  283. if isempty(cell2mat(regexp(jcp,jarfile)))
  284. % Mysql is not on the path
  285. this_file = mfilename('fullpath');
  286. [this_path] = fileparts(this_file);
  287. javaaddpath(fullfile(this_path, jarfile));
  288. end
  289. end