1 /++ 2 Object-Relational Mapping (ORM) 3 +/ 4 module oceandrift.db.orm; 5 6 import std.meta; 7 import std.string; 8 import std.traits; 9 10 import oceandrift.db.dbal.driver; 11 import oceandrift.db.dbal.v4; 12 13 /// 14 public import oceandrift.db.dbal.v4 : asc, desc, distinct, where, whereNot, whereParentheses; 15 16 @safe: 17 18 /++ 19 Determines whether a $(I Database Driver) is compatible with the ORM 20 +/ 21 enum bool isORMCompatible(Driver) = 22 isDatabaseDriver!Driver 23 && isQueryCompiler!Driver; 24 25 /++ 26 Determines whether a type is an entity type suitable for ORM use 27 +/ 28 enum bool isEntityType(TEntity) = ( 29 ( 30 is(TEntity == struct) 31 || is(TEntity == class) 32 ) 33 && is(ReturnType!((TEntity t) => t.id) == ulong) 34 && __traits(compiles, delegate(TEntity t) @safe { t.id = ulong(0); }) 35 ); 36 37 /++ 38 Determines the associated table name for an entity type 39 +/ 40 enum string tableName(alias TEntity) = 41 __traits(identifier, TEntity).toLower; 42 43 /++ 44 Determines the associated join table name for two entity types 45 +/ 46 enum string joinTableName(alias TEntity1, alias TEntity2) = 47 joinTableNameImpl!(TEntity1, TEntity2)(); 48 49 private string joinTableNameImpl(alias TEntity1, alias TEntity2)() 50 { 51 import std.string : cmp; 52 53 string name1 = tableName!TEntity1; 54 string name2 = tableName!TEntity2; 55 56 int x = cmp(name1, name2); 57 58 // dfmt off 59 return (x < 0) 60 ? name1 ~ '_' ~ name2 61 : name2 ~ '_' ~ name1 62 ; 63 // dfmt on 64 } 65 66 /++ 67 Determines the associated column name for a field of an entity type 68 +/ 69 enum columnName(string fieldName) = columnNameImpl(fieldName); 70 71 /// 72 unittest 73 { 74 assert(columnName!"name" == "name"); 75 assert(columnName!"user_id" == "user_id"); 76 77 assert(columnName!"firstName" == "first_name"); 78 assert(columnName!"LastName" == "last_name"); 79 80 assert(columnName!"userID" == "user_id"); 81 assert(columnName!"canEditPosts" == "can_edit_posts"); 82 assert(columnName!"isHTTPS" == "is_https"); 83 assert(columnName!"isHTTPS_URL" == "is_https_url"); 84 } 85 86 private string columnNameImpl(string fieldName) 87 { 88 import std.ascii : isUpper, toLower; 89 90 string output = ""; 91 bool previousWasUpper = false; 92 93 foreach (size_t idx, char c; fieldName) 94 { 95 if (c.isUpper) 96 { 97 if (!previousWasUpper && (idx > 0) && (output[$ - 1] != '_')) 98 output ~= '_'; 99 100 output ~= c.toLower; 101 previousWasUpper = true; 102 continue; 103 } 104 105 previousWasUpper = false; 106 output ~= c; 107 } 108 109 return output[]; 110 } 111 112 /++ 113 Returns: 114 The column names associated with the provided entity type 115 +/ 116 enum auto columnNames(alias TEntity) = 117 aliasSeqOf!(columnNamesImpl!TEntity()); 118 119 /++ 120 Returns: 121 The column names associated with the provided entity type 122 except for the ID column 123 +/ 124 enum auto columnNamesNoID(alias TEntity) = 125 aliasSeqOf!(columnNamesImpl!(TEntity, false)()); 126 127 enum fieldNames(alias TEntity) = 128 aliasSeqOf!(columnNamesImpl!(TEntity, true, false)()); 129 130 enum fieldNamesNoID(alias TEntity) = 131 aliasSeqOf!(columnNamesImpl!(TEntity, false, false)()); 132 133 private auto columnNamesImpl(TEntity, bool includeID = true, bool toColumnNames = true)() 134 { 135 string[] columnNames = []; 136 137 static foreach (idx, field; FieldNameTuple!TEntity) 138 { 139 static assert(isDBValueCompatible!(Fields!TEntity[idx]), "Column not serializable to DBValue"); 140 141 static if (includeID || (field.toLower != "id")) 142 { 143 static if (toColumnNames) 144 columnNames ~= columnName!field; 145 else 146 columnNames ~= field; 147 } 148 } 149 150 return columnNames; 151 } 152 153 /++ 154 Transforms a result row into an instance of the specified entity type 155 +/ 156 TEntity toEntity(TEntity)(Row row) 157 { 158 static if (is(TEntity == class)) 159 TEntity e = new TEntity(); 160 else static if (is(TEntity == struct)) 161 TEntity e = TEntity(); 162 else 163 static assert(0, "faulty template constraint implementation."); 164 165 static foreach (idx, name; fieldNames!TEntity) 166 mixin("e." ~ name) = row[idx].getAs!(typeof(mixin("e." ~ name))); 167 168 return e; 169 } 170 171 /++ 172 Transforms a statement’s result rows into a collection of entities 173 +/ 174 EntityCollection!TEntity toEntities(TEntity)(Statement stmt) pure nothrow @nogc 175 { 176 return EntityCollection!TEntity(stmt); 177 } 178 179 /++ 180 Collection of entities (retrieved through a query) 181 182 Lazy $(I Input Range) implementation. 183 +/ 184 struct EntityCollection(TEntity) if (isEntityType!TEntity) 185 { 186 @safe: 187 188 private 189 { 190 Statement _stmt; 191 } 192 193 private this(Statement stmt) pure nothrow @nogc 194 { 195 _stmt = stmt; 196 } 197 198 /// 199 bool empty() 200 { 201 return _stmt.empty; 202 } 203 204 /// 205 TEntity front() 206 { 207 return _stmt.front.toEntity!TEntity(); 208 } 209 210 /// 211 void popFront() 212 { 213 _stmt.popFront(); 214 } 215 } 216 217 /++ 218 ORM Query Builder 219 220 --- 221 // pre-compiled query (CTFE) 222 enum bqMountains = em.find!Mountain() 223 .orderBy("height") 224 .limit(25) 225 .select(); 226 227 // execute (at runtime) 228 auto mountains = bqMountains.via(db); 229 foreach (mt; mountains) { 230 // […] 231 } 232 --- 233 234 For each member function returning a [BuiltPreCollection], 235 there’s also a shorthand “via” function. 236 The “via” variant executes the built query via the provided database connection. 237 238 --- 239 auto mountains = em.find!Mountain() 240 .orderBy("height") 241 .limit(25) 242 .selectVia(db); 243 244 foreach (mt; mountains) { 245 // […] 246 } 247 --- 248 +/ 249 struct PreCollection(TEntity, DatabaseDriver) 250 if (isEntityType!TEntity && isORMCompatible!DatabaseDriver) 251 { 252 @safe: 253 254 /++ 255 SELECTs the requested entities from the database 256 257 --- 258 auto mountains = em.find!Mountain() 259 .orderBy("height") 260 .limit(25) 261 .selectVia(db); 262 263 foreach (mt; mountains) { 264 // […] 265 } 266 --- 267 +/ 268 BuiltPreCollection!TEntity select() 269 { 270 BuiltQuery bq = _query 271 .select(columnNames!TEntity) 272 .build!DatabaseDriver(); 273 return BuiltPreCollection!TEntity(bq); 274 } 275 276 /// ditto 277 EntityCollection!TEntity selectVia(DatabaseDriver db) 278 { 279 return this.select().via(db); 280 } 281 282 /++ 283 COUNTs the number of requested entities from the database 284 285 --- 286 ulong nMountains = em.find!Mountain().countVia(db); 287 --- 288 +/ 289 BuiltQuery count() 290 { 291 BuiltQuery bq = _query 292 .select(oceandrift.db.dbal.v4.count("*")) 293 .build!DatabaseDriver(); 294 return bq; 295 } 296 297 /// ditto 298 ulong countVia(DatabaseDriver db) 299 { 300 BuiltQuery bq = this.count(); 301 302 Statement stmt = db.prepareBuiltQuery(bq); 303 stmt.execute(); 304 305 debug assert(!stmt.empty); 306 return stmt.front[0].getAs!ulong(); 307 } 308 309 /++ 310 SELECTs aggregate data for the requested entities from the database 311 312 --- 313 DBValue maxHeight = em.find!Mountain().aggregateVia(AggregateFunction.max, "height", db); 314 --- 315 +/ 316 BuiltQuery aggregate(Distinct distinct = Distinct.no)(AggregateFunction aggr, string column) 317 { 318 BuiltQuery bq = _query 319 .select(SelectExpression(col(column), aggr, distinct)) 320 .build!DatabaseDriver(); 321 return bq; 322 } 323 324 /// ditto 325 DBValue aggregateVia(Distinct distinct = Distinct.no)(AggregateFunction aggr, string column, DatabaseDriver db) 326 { 327 BuiltQuery bq = this.aggregate!(distinct)(aggr, column); 328 329 Statement stmt = db.prepareBuiltQuery(bq); 330 stmt.execute(); 331 332 debug assert(!stmt.empty); 333 debug assert(stmt.front.length == 1); 334 return stmt.front[0]; 335 } 336 337 /++ 338 DELETEs the requested entities from the database 339 340 --- 341 em.find!Mountain() 342 .where("height", '<', 3000) 343 .deleteVia(db); 344 --- 345 +/ 346 BuiltQuery delete_() 347 { 348 BuiltQuery bq = _query 349 .delete_() 350 .build!DatabaseDriver(); 351 return bq; 352 } 353 354 /// ditto 355 void deleteVia(DatabaseDriver db) 356 { 357 BuiltQuery bq = this.delete_(); 358 359 Statement stmt = db.prepareBuiltQuery(bq); 360 stmt.execute(); 361 stmt.close(); 362 } 363 364 /++ 365 Specifies the filter criteria for the requested data 366 367 Adds a WHERE clause to the query. 368 369 --- 370 auto children = em.find!Person() 371 .where("age", ComparisonOperator.lessThan, 18) 372 .selectVia(db); 373 --- 374 +/ 375 PreCollection!(TEntity, DatabaseDriver) where(LogicalOperator logicalJunction = and, TComparisonOperator)( 376 string column, TComparisonOperator op, const DBValue value) 377 if (isComparisonOperator!TComparisonOperator) 378 { 379 return typeof(this)(_query.where!logicalJunction(column, op, value)); 380 } 381 382 /// ditto 383 PreCollection!(TEntity, DatabaseDriver) where(LogicalOperator logicalJunction = and, TComparisonOperator, T)( 384 string column, TComparisonOperator op, const T value) 385 if (isComparisonOperator!TComparisonOperator && isDBValueCompatible!T) 386 { 387 return typeof(this)(_query.where!logicalJunction(column, op, value)); 388 } 389 390 /// ditto 391 PreCollection!(TEntity, DatabaseDriver) where(LogicalOperator logicalJunction = and, TComparisonOperator)( 392 string column, TComparisonOperator op) 393 if (isComparisonOperator!TComparisonOperator) 394 { 395 return typeof(this)(_query.where!logicalJunction(column, op, null)); 396 } 397 398 /// ditto 399 PreCollection!(TEntity, DatabaseDriver) whereParentheses(LogicalOperator logicalJunction = and)( 400 Query delegate(Query q) @safe pure conditions) 401 { 402 return typeof(this)(_query.whereParentheses!logicalJunction(conditions)); 403 } 404 405 /++ 406 Specifies a sorting criteria for the requested data 407 +/ 408 PreCollection!(TEntity, DatabaseDriver) orderBy(string column, OrderingSequence orderingSequence = asc) 409 { 410 return typeof(this)(_query.orderBy(column, orderingSequence)); 411 } 412 413 /++ 414 LIMITs the number of entities to retrieve 415 +/ 416 PreCollection!(TEntity, DatabaseDriver) limit(ulong limit) 417 { 418 return typeof(this)(_query.limit(limit)); 419 } 420 421 /// ditto 422 PreCollection!(TEntity, DatabaseDriver) limit(ulong limit, int offset) 423 { 424 return typeof(this)(_query.limit(limit, offset)); 425 } 426 427 private: 428 Query _query; 429 } 430 431 /++ 432 Compiled query statement to use with the ORM 433 +/ 434 struct BuiltPreCollection(TEntity) if (isEntityType!TEntity) 435 { 436 @safe pure nothrow @nogc: 437 438 /// 439 string sql() 440 { 441 return _query.sql; 442 } 443 444 private: 445 BuiltQuery _query; 446 } 447 448 /// 449 struct PreparedCollection(TEntity) if (isEntityType!TEntity) 450 { 451 @safe: 452 453 /// 454 void bind(T)(int index, const T value) 455 { 456 _statement.bind(index, value); 457 } 458 459 /// 460 EntityCollection!TEntity execute() 461 { 462 _statement.execute(); 463 return EntityCollection!TEntity(_statement); 464 } 465 466 private: 467 Statement _statement; 468 } 469 470 PreparedCollection!TEntity prepareCollection(TEntity, DatabaseDriver)( 471 BuiltPreCollection!TEntity builtPreCollection, DatabaseDriver db) 472 if (isDatabaseDriver!DatabaseDriver && isEntityType!TEntity) 473 { 474 return PreparedCollection!TEntity(db.prepareBuiltQuery(builtPreCollection._query)); 475 } 476 477 /++ 478 Retrieves and maps data to the corresponding entity type 479 480 See_Also: [via] 481 +/ 482 EntityCollection!TEntity map(TEntity, DatabaseDriver)(DatabaseDriver db, BuiltPreCollection!TEntity builtPreCollection) 483 if (isDatabaseDriver!DatabaseDriver && isEntityType!TEntity) 484 { 485 pragma(inline, true); 486 return via(builtPreCollection, db); 487 } 488 489 /++ 490 Executes a built query via the provided database connection 491 +/ 492 EntityCollection!TEntity via(TEntity, DatabaseDriver)( 493 BuiltPreCollection!TEntity builtPreCollection, DatabaseDriver db) 494 if (isDatabaseDriver!DatabaseDriver && isEntityType!TEntity) 495 { 496 Statement stmt = db.prepareBuiltQuery(builtPreCollection._query); 497 stmt.execute(); 498 return EntityCollection!TEntity(stmt); 499 } 500 501 /++ 502 Loads an entity from the database 503 +/ 504 bool get(TEntity, DatabaseDriver)(DatabaseDriver db, ulong id, out TEntity output) 505 if (isEntityType!TEntity) 506 { 507 enum BuiltQuery query = table(tableName!TEntity).qb 508 .where("id", '=') 509 .select(columnNames!TEntity) 510 .build!DatabaseDriver(); 511 512 Statement stmt = db.prepareBuiltQuery(query); 513 stmt.bind(0, id); 514 515 stmt.execute(); 516 if (stmt.empty) 517 return false; 518 519 output = stmt.front.toEntity!TEntity; 520 return true; 521 } 522 523 /++ 524 Entity Manager 525 526 Primary object of the ORM 527 +/ 528 struct EntityManager(DatabaseDriver) if (isORMCompatible!DatabaseDriver) 529 { 530 @safe: 531 532 private 533 { 534 DatabaseDriver _db; 535 } 536 537 /// 538 this(DatabaseDriver db) 539 { 540 _db = db; 541 } 542 543 /++ 544 Loads the requested entity (#ID) from the database 545 546 --- 547 Mountain mt; 548 bool found = em.get!Mountain(4, mt); 549 --- 550 +/ 551 bool get(TEntity)(ulong id, out TEntity output) if (isEntityType!TEntity) 552 { 553 enum BuiltQuery query = table(tableName!TEntity).qb 554 .where("id", '=') 555 .select(columnNames!TEntity) 556 .build!DatabaseDriver(); 557 558 Statement stmt = _db.prepareBuiltQuery(query); 559 stmt.bind(0, id); 560 561 stmt.execute(); 562 if (stmt.empty) 563 return false; 564 565 output = stmt.front.toEntity!TEntity; 566 return true; 567 } 568 569 /++ 570 Updates or stores the provided entity in the database 571 +/ 572 void save(TEntity)(ref TEntity entity) if (isEntityType!TEntity) 573 { 574 if (entity.id == 0) 575 entity.id = this.store(entity); 576 else 577 this.update(entity); 578 } 579 580 /++ 581 Stores the provided entity in the database 582 583 Does not set the entity ID. Returns it instead. 584 This allows you to use the entity as a template. 585 586 See_Also: 587 [EntityManager.save|save] 588 +/ 589 ulong store(TEntity)(const TEntity entity) if (isEntityType!TEntity) 590 { 591 enum BuiltQuery query = table(tableName!TEntity) 592 .insert(columnNamesNoID!TEntity) 593 .build!DatabaseDriver(); 594 595 Statement stmt = _db.prepareBuiltQuery(query); 596 597 static foreach (int idx, column; fieldNamesNoID!TEntity) 598 stmt.bind(idx, mixin("cast(const) entity." ~ column)); 599 600 stmt.execute(); 601 602 return _db.lastInsertID().getAs!ulong; 603 } 604 605 /++ 606 Updates the provided entity in the database 607 608 See_Also: 609 [EntityManager.save|save] 610 +/ 611 void update(TEntity)(const TEntity entity) if (isEntityType!TEntity) 612 in (entity.id != 0) 613 { 614 enum BuiltQuery query = table(tableName!TEntity).qb 615 .where("id", '=') 616 .update(columnNamesNoID!TEntity) 617 .build!DatabaseDriver(); 618 619 Statement stmt = _db.prepareBuiltQuery(query); 620 621 static foreach (int idx, column; columnNamesNoID!TEntity) 622 mixin("stmt.bind(idx, cast(const) entity." ~ column ~ ");"); 623 624 enum int idParamN = columnNamesNoID!TEntity.length; 625 stmt.bind(idParamN, entity.id); 626 627 stmt.execute(); 628 } 629 630 /++ 631 Removes (deletes) the provided entity from the database 632 +/ 633 void remove(TEntity)(ulong id) if (isEntityType!TEntity) 634 { 635 enum BuiltQuery query = table(tableName!TEntity).qb 636 .where("id", '=') 637 .delete_() 638 .build!DatabaseDriver(); 639 640 Statement stmt = _db.prepareBuiltQuery(query); 641 stmt.bind(0, id); 642 stmt.execute(); 643 } 644 645 /// ditto 646 void remove(TEntity)(TEntity entity) if (isEntityType!TEntity) 647 { 648 return this.remove!TEntity(entity.id); 649 } 650 651 deprecated EntityCollection!TEntity find(TEntity)(Query delegate(Query) @safe buildQuery) 652 if (isEntityType!TEntity) 653 in (buildQuery !is null) 654 { 655 Query q = table(tableName!TEntity).qb; 656 q = buildQuery(q); 657 BuiltQuery query = q 658 .select(columnNames!TEntity) 659 .build!DatabaseDriver(); 660 661 Statement stmt = _db.prepareBuiltQuery(query); 662 stmt.execute(); 663 664 return EntityCollection!TEntity(stmt); 665 } 666 667 deprecated EntityCollection!TEntity find(TEntity, Query function(Query) @safe pure buildQuery)( 668 void delegate(Statement) @safe bindValues = null) 669 if (isEntityType!TEntity && (buildQuery !is null)) 670 { 671 enum Query q = buildQuery(table(tableName!TEntity).qb); 672 enum BuiltQuery query = q 673 .select(columnNames!TEntity) 674 .build!DatabaseDriver(); 675 676 Statement stmt = _db.prepareBuiltQuery(query); 677 678 if (bindValues !is null) 679 bindValues(stmt); 680 681 stmt.execute(); 682 683 return EntityCollection!TEntity(stmt); 684 } 685 686 /++ 687 Finds entites from the database 688 689 Query building starting point. 690 +/ 691 static PreCollection!(TEntity, DatabaseDriver) find(TEntity)() 692 if (isEntityType!TEntity) 693 { 694 enum Query q = table(tableName!TEntity).qb; 695 enum pc = PreCollection!(TEntity, DatabaseDriver)(q); 696 return pc; 697 } 698 699 /// 700 bool manyToOne(TEntityOne, TEntityMany)(TEntityMany many, out TEntityOne output) 701 if (isEntityType!TEntityOne && isEntityType!TEntityMany) 702 { 703 enum BuiltQuery bq = table(tableName!TEntityOne).qb 704 .where("id", '=') 705 .select(columnNames!TEntityOne) 706 .build!DatabaseDriver(); 707 708 Statement stmt = _db.prepareBuiltQuery(bq); 709 immutable ulong oneID = mixin("many." ~ tableName!TEntityOne ~ "ID"); 710 stmt.bind(0, oneID); 711 stmt.execute(); 712 713 if (stmt.empty) 714 return false; 715 716 output = stmt.front.toEntity!TEntityOne(); 717 return true; 718 } 719 720 /// 721 bool oneToOne(TEntityTarget, TEntitySource)(TEntitySource source, out TEntityTarget toOne) 722 if (isEntityType!TEntityTarget && isEntityType!TEntitySource) 723 { 724 pragma(inline, true); 725 return manyToOne(source, toOne); 726 } 727 728 /// 729 static PreCollection!(TEntityTarget, DatabaseDriver) manyToMany( 730 TEntityTarget, 731 TEntitySource, 732 string joinTableName_ = joinTableName!(TEntitySource, TEntityTarget) 733 )(TEntitySource source) 734 if (isEntityType!TEntityTarget && isEntityType!TEntitySource) 735 { 736 enum Table joinTable = table(joinTableName_); 737 738 enum string targetName = tableName!TEntityTarget; 739 enum Column columnForeignKeyTarget = col(joinTable, targetName ~ "_id"); 740 741 enum string sourceName = tableName!TEntitySource; 742 enum Column columnForeignKeySource = col(joinTable, sourceName ~ "_id"); 743 744 enum Column columnPrimaryKeyTarget = col(table(targetName), "id"); 745 746 enum Query q = 747 joinTable.qb 748 .join( 749 columnPrimaryKeyTarget, 750 columnForeignKeyTarget 751 ) 752 .where(columnForeignKeySource, '='); 753 enum pcT = PreCollection!(TEntityTarget, DatabaseDriver)(q); 754 755 auto pc = pcT; 756 pc._query.updatePreSetWhereValue(0, DBValue(source.id)); 757 return pc; 758 } 759 760 /++ 761 Assigns two entities with a many-to-many relation to each other 762 +/ 763 void manyToManyAssign( 764 TEntity1, 765 TEntity2, 766 string joinTableName_ = joinTableName!(TEntity1, TEntity2), 767 )(TEntity1 e1, TEntity2 e2) if (isEntityType!TEntity1 && isEntityType!TEntity2) 768 { 769 enum Table joinTable = table(joinTableName_); 770 enum string e2Col = tableName!TEntity2 ~ "_id"; 771 enum string e1Col = tableName!TEntity1 ~ "_id"; 772 773 enum BuiltQuery bq = joinTable.insert(e1Col, e2Col).build!DatabaseDriver(); 774 775 Statement stmt = _db.prepareBuiltQuery(bq); 776 stmt.bind(0, e1.id); 777 stmt.bind(1, e2.id); 778 stmt.execute(); 779 } 780 781 /++ 782 Deletes the association from each other of two entities with a many-to-many relation 783 +/ 784 void manyToManyUnassign( 785 TEntity1, 786 TEntity2, 787 string joinTableName_ = joinTableName!(TEntity1, TEntity2), 788 )(TEntity1 e1, TEntity2 e2) if (isEntityType!TEntity1 && isEntityType!TEntity2) 789 { 790 enum Table joinTable = table(joinTableName!(TEntity1, TEntity2)); 791 enum string e2Col = tableName!TEntity2 ~ "_id"; 792 enum string e1Col = tableName!TEntity1 ~ "_id"; 793 794 enum BuiltQuery bq = joinTable.qb 795 .where(e1Col, '=') 796 .where(e2Col, '=') 797 .delete_() 798 .build!DatabaseDriver(); 799 800 Statement stmt = _db.prepareBuiltQuery(bq); 801 stmt.bind(0, e1.id); 802 stmt.bind(1, e2.id); 803 stmt.execute(); 804 } 805 806 /// 807 static PreCollection!(TEntityMany, DatabaseDriver) oneToMany( 808 TEntityMany, 809 TEntityOne, 810 )(TEntityOne source) if (isEntityType!TEntityMany && isEntityType!TEntityOne) 811 { 812 enum string foreignKeyColumn = tableName!TEntityOne ~ "_id"; 813 814 enum Query q = table(tableName!TEntityMany).qb.where(foreignKeyColumn, '='); 815 enum pcT = PreCollection!(TEntityMany, DatabaseDriver)(q); 816 817 auto pc = pcT; 818 pc._query.updatePreSetWhereValue(0, DBValue(source.id)); 819 return pc; 820 } 821 822 // debugging helper 823 static void _pragma(TEntity)() 824 { 825 pragma(msg, "==== EntityManager._pragma!(" ~ TEntity.stringof ~ "):"); 826 827 static if (!isEntityType!TEntity) 828 { 829 pragma(msg, "- ERROR: Not a compatible type"); 830 } 831 else 832 { 833 pragma(msg, "- Table Name:\n" ~ tableName!TEntity); 834 pragma(msg, "- Column Names:"); 835 pragma(msg, columnNames!TEntity); 836 pragma(msg, "- Column Names (no ID):"); 837 pragma(msg, columnNamesNoID!TEntity); 838 } 839 840 pragma(msg, "/===="); 841 } 842 } 843 844 /++ 845 Mixin template to add the entity ID member to a type 846 847 --- 848 struct MyEntity { 849 mixin EntityID; 850 } 851 --- 852 853 $(TIP 854 You don’t have to use this, but it might help with reduction of boilerplate code. 855 ) 856 +/ 857 mixin template EntityID() 858 { 859 ulong id = 0; 860 }