1 module mysql.data_object; 2 3 import std.string; 4 import mysql.database; 5 6 /* 7 This is like a result set 8 9 10 DataObject res = [...]; 11 12 res.name = "Something"; 13 14 res.commit; // runs the actual update or insert 15 16 17 res = new DataObject(fields, tables 18 19 20 21 22 23 24 25 when doing a select, we need to figure out all the tables and modify the query to include the ids we need 26 27 28 search for FROM and JOIN 29 the next token is the table name 30 31 right before the FROM, add the ids of each table 32 33 34 given: 35 SELECT name, phone FROM customers LEFT JOIN phones ON customer.id = phones.cust_id 36 37 we want: 38 SELECT name, phone, customers.id AS id_from_customers, phones.id AS id_from_phones FROM customers LEFT JOIN phones ON customer.id[...]; 39 40 */ 41 42 mixin template DataObjectConstructors() { 43 this(Database db, string[string] res, Tuple!(string, string)[string] mappings) { 44 super(db, res, mappings); 45 } 46 } 47 48 string yield(string what) { return `if(auto result = dg(`~what~`)) return result;`; } 49 50 import std.typecons; 51 import std.json; // for json value making 52 53 class DataObject { 54 // lets you just free-form set fields, assuming they all come from the given table 55 // note it doesn't try to handle joins for new rows. you've gotta do that yourself 56 this(Database db, string table) { 57 assert(db !is null); 58 this.db = db; 59 this.table = table; 60 61 mode = UpdateOrInsertMode.CheckForMe; 62 } 63 64 JSONValue makeJsonValue() { 65 JSONValue val; 66 JSONValue[string] valo; 67 //val.type = JSON_TYPE.OBJECT; 68 foreach(k, v; fields) { 69 JSONValue s; 70 //s.type = JSON_TYPE.STRING; 71 s.str = v; 72 valo[k] = s; 73 //val.object[k] = s; 74 } 75 val = valo; 76 return val; 77 } 78 79 this(Database db, string[string] res, Tuple!(string, string)[string] mappings) { 80 this.db = db; 81 this.mappings = mappings; 82 this.fields = res; 83 84 mode = UpdateOrInsertMode.AlwaysUpdate; 85 } 86 87 string table; 88 // table, column [alias] 89 Tuple!(string, string)[string] mappings; 90 91 // value [field] [table] 92 string[string][string] multiTableKeys; // note this is not set internally tight now 93 // but it can be set manually to do multi table mappings for automatic update 94 95 96 string opDispatch(string field, string file = __FILE__, size_t line = __LINE__)() 97 if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront") 98 { 99 if(field !in fields) 100 throw new Exception("no such field " ~ field, file, line); 101 102 return fields[field]; 103 } 104 105 string opDispatch(string field, T)(T t) 106 if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront") 107 { 108 static if(__traits(compiles, t is null)) { 109 if(t is null) 110 setImpl(field, null); 111 else 112 setImpl(field, to!string(t)); 113 } else 114 setImpl(field, to!string(t)); 115 116 return fields[field]; 117 } 118 119 120 // vararg hack so property assignment works right, even with null 121 version(none) 122 string opDispatch(string field, string file = __FILE__, size_t line = __LINE__)(...) 123 if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront") 124 { 125 if(_arguments.length == 0) { 126 if(field !in fields) 127 throw new Exception("no such field " ~ field, file, line); 128 129 return fields[field]; 130 } else if(_arguments.length == 1) { 131 auto arg = _arguments[0]; 132 133 string a; 134 if(arg == typeid(string) || arg == typeid(immutable(string)) || arg == typeid(const(immutable(char)[]))) { 135 a = va_arg!(string)(_argptr); 136 } else if (arg == typeid(int) || arg == typeid(immutable(int)) || arg == typeid(const(int))) { 137 auto e = va_arg!(int)(_argptr); 138 a = to!string(e); 139 } else if (arg == typeid(char) || arg == typeid(immutable(char))) { 140 auto e = va_arg!(char)(_argptr); 141 a = to!string(e); 142 } else if (arg == typeid(long) || arg == typeid(const(long)) || arg == typeid(immutable(long))) { 143 auto e = va_arg!(long)(_argptr); 144 a = to!string(e); 145 } else if (arg == typeid(null)) { 146 a = null; 147 } else assert(0, "invalid type " ~ arg.toString ); 148 149 150 auto setTo = a; 151 setImpl(field, setTo); 152 153 return setTo; 154 155 } else assert(0, "too many arguments"); 156 157 assert(0); // should never be reached 158 } 159 160 private void setImpl(string field, string value) { 161 if(field in fields) { 162 if(fields[field] != value) 163 changed[field] = true; 164 } else { 165 changed[field] = true; 166 } 167 168 fields[field] = value; 169 } 170 171 public void setWithoutChange(string field, string value) { 172 fields[field] = value; 173 } 174 175 int opApply(int delegate(ref string) dg) { 176 foreach(a; fields) 177 mixin(yield("a")); 178 179 return 0; 180 } 181 182 int opApply(int delegate(ref string, ref string) dg) { 183 foreach(a, b; fields) 184 mixin(yield("a, b")); 185 186 return 0; 187 } 188 189 190 string opIndex(string field, string file = __FILE__, size_t line = __LINE__) { 191 if(field !in fields) 192 throw new DatabaseException("No such field in data object: " ~ field, file, line); 193 return fields[field]; 194 } 195 196 string opIndexAssign(string value, string field) { 197 setImpl(field, value); 198 return value; 199 } 200 201 string* opBinary(string op)(string key) if(op == "in") { 202 return key in fields; 203 } 204 205 string[string] fields; 206 bool[string] changed; 207 208 void commitChanges() { 209 commitChanges(cast(string) null, null); 210 } 211 212 void commitChanges(string key, string keyField) { 213 commitChanges(key is null ? null : [key], keyField is null ? null : [keyField]); 214 } 215 216 void commitChanges(string[] keys, string[] keyFields = null) { 217 string[string][string] toUpdate; 218 int updateCount = 0; 219 foreach(field, c; changed) { 220 if(c) { 221 string tbl, col; 222 if(mappings is null) { 223 tbl = this.table; 224 col = field; 225 } else { 226 if(field !in mappings) 227 assert(0, "no such mapping for " ~ field); 228 auto m = mappings[field]; 229 tbl = m[0]; 230 col = m[1]; 231 } 232 233 toUpdate[tbl][col] = fields[field]; 234 updateCount++; 235 } 236 } 237 238 if(updateCount) { 239 db.startTransaction(); 240 scope(success) db.query("COMMIT"); 241 scope(failure) db.query("ROLLBACK"); 242 243 foreach(tbl, values; toUpdate) { 244 string where, keyFieldToPass; 245 246 if(keys is null) { 247 keys = [null]; 248 } 249 250 if(multiTableKeys is null || tbl !in multiTableKeys) 251 foreach(i, key; keys) { 252 string keyField; 253 254 if(key is null) { 255 key = "id_from_" ~ tbl; 256 if(key !in fields) 257 key = "id"; 258 } 259 260 if(i >= keyFields.length || keyFields[i] is null) { 261 if(key == "id_from_" ~ tbl) 262 keyField = "id"; 263 else 264 keyField = key; 265 } else { 266 keyField = keyFields[i]; 267 } 268 269 270 if(where.length) 271 where ~= " AND "; 272 273 auto f = key in fields ? fields[key] : null; 274 if(f is null) 275 where ~= keyField ~ " = NULL"; 276 else 277 where ~= keyField ~ " = '"~db.escape(f)~"'" ; 278 if(keyFieldToPass.length) 279 keyFieldToPass ~= ", "; 280 281 keyFieldToPass ~= keyField; 282 } 283 else { 284 foreach(keyField, v; multiTableKeys[tbl]) { 285 if(where.length) 286 where ~= " AND "; 287 288 where ~= keyField ~ " = '"~db.escape(v)~"'" ; 289 if(keyFieldToPass.length) 290 keyFieldToPass ~= ", "; 291 292 keyFieldToPass ~= keyField; 293 } 294 } 295 296 297 298 updateOrInsert(db, tbl, values, where, mode, keyFieldToPass); 299 } 300 301 changed = null; 302 } 303 } 304 305 void commitDelete() { 306 if(mode == UpdateOrInsertMode.AlwaysInsert) 307 throw new Exception("Cannot delete an item not in the database"); 308 309 assert(table.length); // FIXME, should work with fancy items too 310 311 // FIXME: escaping and primary key questions 312 db.query("DELETE FROM " ~ table ~ " WHERE id = '" ~ db.escape(fields["id"]) ~ "'"); 313 } 314 315 string getAlias(string table, string column) { 316 string ali; 317 if(mappings is null) { 318 if(this.table is null) { 319 mappings[column] = tuple(table, column); 320 return column; 321 } else { 322 assert(table == this.table); 323 ali = column; 324 } 325 } else { 326 foreach(a, what; mappings) 327 if(what[0] == table && what[1] == column 328 && a.indexOf("id_from_") == -1) { 329 ali = a; 330 break; 331 } 332 } 333 334 return ali; 335 } 336 337 void set(string table, string column, string value) { 338 string ali = getAlias(table, column); 339 //assert(ali in fields); 340 setImpl(ali, value); 341 } 342 343 string select(string table, string column) { 344 string ali = getAlias(table, column); 345 //assert(ali in fields); 346 if(ali in fields) 347 return fields[ali]; 348 return null; 349 } 350 351 DataObject addNew() { 352 auto n = new DataObject(db, null); 353 354 n.db = this.db; 355 n.table = this.table; 356 n.mappings = this.mappings; 357 358 foreach(k, v; this.fields) 359 if(k.indexOf("id_from_") == -1) 360 n.fields[k] = v; 361 else 362 n.fields[k] = null; // don't copy ids 363 364 n.mode = UpdateOrInsertMode.AlwaysInsert; 365 366 return n; 367 } 368 369 Database db; 370 UpdateOrInsertMode mode; 371 } 372 373 /** 374 You can subclass DataObject if you want to 375 get some compile time checks or better types. 376 377 You'll want to disable opDispatch, then forward your 378 properties to the super opDispatch. 379 */ 380 381 /*mixin*/ string DataObjectField(T, string table, string column, string aliasAs = null)() { 382 string aliasAs_; 383 if(aliasAs is null) 384 aliasAs_ = column; 385 else 386 aliasAs_ = aliasAs; 387 return ` 388 @property void `~aliasAs_~`(`~T.stringof~` setTo) { 389 super.set("`~table~`", "`~column~`", to!string(setTo)); 390 } 391 392 @property `~T.stringof~` `~aliasAs_~` () { 393 return to!(`~T.stringof~`)(super.select("`~table~`", "`~column~`")); 394 } 395 `; 396 } 397 398 mixin template StrictDataObject() { 399 // disable opdispatch 400 string opDispatch(string name)(...) if (0) {} 401 } 402 403 404 string createDataObjectFieldsFromAlias(string table, fieldsToUse)() { 405 string ret; 406 407 fieldsToUse f; 408 foreach(member; __traits(allMembers, fieldsToUse)) { 409 ret ~= DataObjectField!(typeof(__traits(getMember, f, member)), table, member); 410 } 411 412 return ret; 413 } 414 415 416 /** 417 This creates an editable data object out of a simple struct. 418 419 struct MyFields { 420 int id; 421 string name; 422 } 423 424 alias SimpleDataObject!("my_table", MyFields) User; 425 426 427 User a = new User(db); 428 429 a.id = 30; 430 a.name = "hello"; 431 a.commitChanges(); // tries an update or insert on the my_table table 432 433 434 Unlike the base DataObject class, this template provides compile time 435 checking for types and names, based on the struct you pass in: 436 437 a.id = "aa"; // compile error 438 439 a.notAField; // compile error 440 */ 441 class SimpleDataObject(string tableToUse, fieldsToUse) : DataObject { 442 mixin StrictDataObject!(); 443 444 mixin(createDataObjectFieldsFromAlias!(tableToUse, fieldsToUse)()); 445 446 this(Database db) { 447 super(db, tableToUse); 448 } 449 }