1 module dpq.relationproxy; 2 3 import dpq.attributes; 4 import dpq.connection; 5 import dpq.querybuilder; 6 import dpq.value : Value; 7 8 import std.algorithm : map; 9 import std.array; 10 import std.meta : Alias; 11 import std.typecons : Nullable; 12 13 /** 14 Verifies that the given type can be used a relation 15 */ 16 template IsValidRelation(T) 17 { 18 import std.traits : hasUDA; 19 20 // Class or struct, and has a RelationAttribute. 21 enum IsValidRelation = (is(T == struct) || is(T == class)) && 22 hasUDA!(T, RelationAttribute); 23 } 24 25 26 /** 27 A structure proxying to the actual Postgres table. 28 29 Allows short and readable querying, returning actual records or arrays of 30 them. first, last, and all will be cached if appropriate, the rest will 31 execute a query whenever a result is needed. 32 Most functions return a reference to the same object, allowing you to 33 chain them. Builder pattern, really. 34 35 Can be implicitly converted to an array of the requested structure. 36 37 Mostly meant to be used with RelationMixin, but can be used manually. 38 39 Examples: 40 --------------- 41 struct User 42 { 43 // provides where, find, findBy, etc on the User struct 44 mixin RelationMixin; 45 46 @PK @serial int id' 47 string username; 48 } 49 50 auto user1 = User.where(["id": 1]).first; 51 // Or better yet. Search directly by PK 52 auto betterUser1 = User.find(1); 53 // Delete some users 54 long nDeleted = User.where("username LIKE 'somethi%'").removeAll(); 55 --------------- 56 */ 57 struct RelationProxy(T) 58 if (IsValidRelation!T) 59 { 60 private 61 { 62 /** 63 The actual content, if any at all. 64 */ 65 T[] _content; 66 67 Connection _connection; 68 69 /** 70 QueryBuilder used to generate the required queries for content. 71 72 Sensible defaults. 73 */ 74 QueryBuilder _queryBuilder = QueryBuilder().select(AttributeList!T).from!T; 75 76 /** 77 Signifies whether the content is fresh. The content is not considered 78 fresh once any filters change or new ones are applied. 79 */ 80 bool _contentFresh = false; 81 82 /** 83 Update the content, does not check for freshness 84 */ 85 void _updateContent() 86 { 87 auto result = _queryBuilder.query(_connection).run(); 88 89 _content = result.map!(deserialise!T).array; 90 _markFresh(); 91 } 92 93 /** 94 Mark the content stale, meaning it will be reloaded when next needed. 95 */ 96 void _markStale() 97 { 98 _contentFresh = false; 99 } 100 101 /** 102 Mark the content fresh, it should not be reloaded unless filters change. 103 */ 104 void _markFresh() 105 { 106 _contentFresh = true; 107 } 108 } 109 110 /** 111 Basic constructor. RelationProxy requires a connection to operate on. 112 */ 113 this(ref Connection connection) 114 { 115 _connection = connection; 116 } 117 118 119 /** 120 Makes a copy of just the filters, not any content; 121 */ 122 @property RelationProxy!T dup() 123 { 124 auto copy = RelationProxy!T(_connection); 125 copy._queryBuilder = _queryBuilder; 126 127 return copy; 128 } 129 130 /** 131 Reloads the content with existing filters. 132 */ 133 ref auto reload() 134 { 135 _updateContent(); 136 return this; 137 } 138 139 /** 140 Returns the actual content, executing the query if data has not yet been 141 fetched from the database. 142 */ 143 @property T[] all() 144 { 145 // Update the content if it's not currently fresh or has not been fetched yet 146 if (!_contentFresh) 147 _updateContent(); 148 149 return _content; 150 } 151 152 alias all this; 153 154 /** 155 Specifies filters according to the given AA. 156 Filters will be joined with AND. 157 */ 158 ref auto where(U)(U[string] filters) 159 { 160 _markStale(); 161 _queryBuilder.where(filters); 162 return this; 163 } 164 165 /** 166 Same as above, but allowing you to specify a completely custom filter. 167 The supplied string will be wrapped in (), can be called multiple times 168 to specify multiple filters. 169 */ 170 ref auto where(U...)(string filter, U params) 171 { 172 _markStale(); 173 _queryBuilder.where(filter, params); 174 return this; 175 } 176 177 /** 178 Convenience alias, allows us to do proxy.where(..).and(...) 179 */ 180 alias and = where; 181 182 /** 183 Inserts an OR seperator between the filters. 184 185 Examples: 186 ------------------ 187 proxy.where("something").and("something2").or("something3"); 188 // will produce "WHERE ((something) AND (something2)) OR (something3)" 189 ------------------ 190 */ 191 @property ref auto or() 192 { 193 _queryBuilder.or(); 194 return this; 195 } 196 197 /** 198 Fetches the first record matching the filters. 199 200 Will return a Nullable null value if no matches. 201 202 If data is already cached and not marked stale/unfresh, it will reuse it, 203 meaning that calling this after calling all will not generate an additional 204 query, even if called multiple times. 205 Will not cache its own result, only reuse existing data. 206 207 Params: 208 by = optional, specifies the column to order by, defaults to PK name 209 order = Order to sort by, (Order.asc, Order.desc), optional, defaults to asc 210 filters = optional filters to apply 211 212 Example: 213 ----------------- 214 auto p = RelationProxy!User(); 215 auto user = p.where(["something": 123]).first; 216 ----------------- 217 */ 218 @property Nullable!T first() 219 { 220 alias RT = Nullable!T; 221 222 // If the content is fresh, we do not have to fetch anything 223 if (_contentFresh) 224 { 225 if (_content.length == 0) 226 return RT.init; 227 return RT(_content[0]); 228 } 229 230 // Make a copy of the builder, as to not ruin the query in case of reuse 231 auto qb = _queryBuilder; 232 qb.limit(1).order(primaryKeyAttributeName!T, Order.asc); 233 234 auto result = qb.query(_connection).run(); 235 236 if (result.rows == 0) 237 return RT.init; 238 239 return RT(result[0].deserialise!T); 240 } 241 242 /** 243 Same as first, but defaults to desceding order, giving you the last match. 244 245 Caching acts the same as with first. 246 */ 247 @property Nullable!T last(string by = primaryKeyAttributeName!T) 248 { 249 alias RT = Nullable!T; 250 251 // If the content is fresh, we do not have to fetch anything 252 if (_contentFresh) 253 { 254 if (_content.length == 0) 255 return RT.init; 256 return RT(_content[$ - 1]); 257 } 258 259 // Make a copy of the builder, as to not ruin the query in case of reuse 260 auto qb = _queryBuilder; 261 qb.limit(1).order(primaryKeyAttributeName!T, Order.desc); 262 263 auto result = qb.query(_connection).run(); 264 265 if (result.rows == 0) 266 return RT.init; 267 268 return RT(result[result.rows - 1].deserialise!T); 269 } 270 271 /** 272 Finds a single record matching the filters. Equivalent to where(...).first. 273 274 Does not change the filters for the RelationProxy it was called on. 275 */ 276 Nullable!T findBy(U)(U[string] filters) 277 { 278 // Make a copy so we do not destroy our filters in the case of reuse. 279 return this.dup.where(filters).first; 280 } 281 282 /** 283 Same as above, but always searches by the primary key 284 */ 285 Nullable!T find(U)(U param) 286 { 287 return findBy([primaryKeyAttributeName!T: param]); 288 } 289 290 291 /** 292 Update all the records matching filters to new values from the AA. 293 294 Does not use IDs of any existing cached data, simply uses the specified 295 filter in an UPDATE query. 296 297 Examples: 298 ---------------- 299 auto p = RelationProxy!User(db); 300 p.where("posts > 500").updateAll(["rank": "Frequent Poster]); 301 ---------------- 302 */ 303 auto updateAll(U)(U[string] updates) 304 { 305 _markStale(); 306 307 auto result = _queryBuilder.dup 308 .update!T 309 .set(updates) 310 .query(_connection).run(); 311 312 return result.rows; 313 } 314 315 auto update(U, Tpk)(Tpk id, U[string] values) 316 { 317 _markStale(); 318 319 auto qb = QueryBuilder() 320 .update!T 321 .set(values) 322 .where([primaryKeyAttributeName!T: Value(id)]); 323 324 return qb.query(_connection).run().rows; 325 } 326 327 /** 328 Simply deletes all the records matching the filters. 329 330 Even if any data is already cached, does not filter by IDs, but simply 331 uses the specified filters in a DELETE query. 332 */ 333 auto removeAll() 334 { 335 _markStale(); 336 337 auto result = _queryBuilder.dup 338 .remove() 339 .query(_connection).run(); 340 341 return result.rows; 342 } 343 344 /** 345 Removes a single record, filtering it by the primary key's value. 346 */ 347 auto remove(Tpk)(Tpk id) 348 { 349 _markStale(); 350 351 auto qb = QueryBuilder() 352 .remove() 353 .from!T 354 .where([primaryKeyAttributeName!T: Value(id)]); 355 356 357 return qb.query(_connection).run().rows; 358 } 359 360 /** 361 Inserts a new record, takes the record as a reference in order to update 362 its PK value. Does not update any other values. 363 364 Does not check if the record is already in the database, simply creates 365 an insert without the PK and can therefore be used to insert the same 366 record multiple times. 367 */ 368 ref T insert(ref T record) 369 { 370 _markStale(); 371 372 enum pk = primaryKeyName!T; 373 enum pkAttr = primaryKeyAttributeName!T; 374 375 auto qb = QueryBuilder() 376 .insert(relationName!T, AttributeList!(T, true, true)) 377 .returning(pkAttr) 378 .addValues!T(record); 379 380 alias pkMem = Alias!(__traits(getMember, record, pk)); 381 auto result = qb.query(_connection).run(); 382 __traits(getMember, record, pk) = result[0][pkAttr].as!(typeof(pkMem)); 383 384 return record; 385 } 386 387 /** 388 Updates the given record in the DB with all the current values. 389 390 Updates by ID. Assumes the record is already in the DB. Does not insert 391 under any circumstance. 392 393 Examples: 394 ----------------- 395 User user = User.first; 396 user.posts = posts - 2; 397 user.save(); 398 ----------------- 399 */ 400 bool save(T record) 401 { 402 _markStale(); 403 404 enum pkName = primaryKeyName!T; 405 406 auto qb = QueryBuilder() 407 .update(relationName!T) 408 .where([pkName: __traits(getMember, record, pkName)]); 409 410 foreach (member; serialisableMembers!T) 411 qb.set( 412 attributeName!(__traits(getMember, record, member)), 413 __traits(getMember, record, member)); 414 415 return qb.query(_connection).run().rows > 0; 416 } 417 418 /** 419 Selects the count of records with current filters. 420 Default is *, but any column can be specified as a parameter. The column 421 name is not further escaped. 422 423 Does not cache the result or use existing data's length. Call .length to 424 get local data's length. 425 */ 426 long count(string col = "*") 427 { 428 import std.string : format; 429 430 auto qb = _queryBuilder.dup 431 .select("count(%s)".format(col)); 432 433 auto result = qb.query(_connection).run(); 434 return result[0][0].as!long; 435 } 436 437 /** 438 A basic toString implementation, mostly here to prevent querying when 439 the object is implicitly converted to a string. 440 */ 441 @property string toString() 442 { 443 return "<RelationProxy!" ~ T.stringof ~ "::`" ~ _queryBuilder.command ~ "`>"; 444 } 445 } 446 447 448 /**************************************************/ 449 /* Methods meant to be used on records themselves */ 450 /**************************************************/ 451 452 /** 453 Reloads the record from the DB, overwrites it. Returns a reference to the 454 same object. 455 456 Examples: 457 --------------------- 458 User user = User.first; 459 writeln("My user is: ", user); 460 user.update(["username": "Oopdated Ooser"]); 461 writeln("My user is: ", user); 462 writeln("My user from DB is: ", User.find(user.id)); 463 writeln("Reloaded user ", user.reload); // will be same as above 464 --------------------- 465 */ 466 ref T reload(T)(ref T record) 467 if (IsValidRelation!T) 468 { 469 enum pkName = primaryKeyName!T; 470 record = T.find(__traits(getMember, record, pkName)); 471 472 return record; 473 } 474 475 /** 476 Updates a single record with the new values, does not set them on the record 477 itself. 478 479 Examples: 480 -------------------- 481 User user = User.first; 482 user.update(["username": "Some new name"]); // will run an UPDATE query 483 user.reload(); // Can be reloaded after to fetch new data from DB if needed 484 -------------------- 485 486 */ 487 auto update(T, U)(T record, U[string] values) 488 if (IsValidRelation!T) 489 { 490 enum pkName = primaryKeyName!T; 491 492 return T.updateOne(__traits(getMember, record, pkName), values); 493 } 494 495 /** 496 Removes a record from the DB, filtering by the primary key. 497 */ 498 auto remove(T)(T record) 499 if (IsValidRelation!T) 500 { 501 enum pkName = primaryKeyName!T; 502 503 return T.removeOne(__traits(getMember, record, pkName)); 504 } 505 506 /** 507 See: RelationProxy's save method 508 */ 509 bool save(T)(T record) 510 if (IsValidRelation!T) 511 { 512 return T.saveRecord(record); 513 }