1 /** 2 * DStruct - Object-Relation Mapping for D programming language, with interface similar to Hibernate. 3 * 4 * Hibernate documentation can be found here: 5 * $(LINK http://hibernate.org/docs)$(BR) 6 * 7 * Source file dstruct/session.d. 8 * 9 * This module contains implementation of DStruct SessionFactory and Session classes. 10 * 11 * Copyright: Copyright 2013 12 * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 13 * Author: Vadim Lopatin 14 */ 15 module dstruct.session; 16 17 import std.algorithm; 18 import std.conv; 19 import std.stdio; 20 import std.exception; 21 import std.variant; 22 23 import dstruct.ddbc.core; 24 import dstruct.ddbc.common; 25 26 import dstruct.type; 27 import dstruct.dialect; 28 import dstruct.core; 29 import dstruct.metadata; 30 import dstruct.query; 31 32 // For backwards compatibily 33 // 'enforceEx' will be removed with 2.089 34 static if (__VERSION__ < 2080) 35 { 36 alias enforceHelper = enforceEx; 37 } 38 else 39 { 40 alias enforceHelper = enforce; 41 } 42 43 const TRACE_REFS = false; 44 45 /// Factory to create DStruct Sessions - similar to org.hibernate.SessionFactory 46 interface SessionFactory 47 { 48 /// close all active sessions 49 void close(); 50 /// check if session factory is closed 51 bool isClosed(); 52 /// creates new session 53 Session openSession(); 54 /// retrieve information about tables and indexes for schema 55 DBInfo getDBMetaData(); 56 } 57 58 /// Session - main interface to load and persist entities -- similar to org.hibernate.Session 59 abstract class Session 60 { 61 /// returns metadata 62 EntityMetaData getMetaData(); 63 64 /// not supported in current implementation 65 Transaction beginTransaction(); 66 /// not supported in current implementation 67 void cancelQuery(); 68 /// not supported in current implementation 69 void clear(); 70 71 /// closes session 72 Connection close(); 73 74 ///Does this session contain any changes which must be synchronized with the database? In other words, would any DML operations be executed if we flushed this session? 75 bool isDirty(); 76 /// Check if the session is still open. 77 bool isOpen(); 78 /// Check if the session is currently connected. 79 bool isConnected(); 80 /// Check if this instance is associated with this Session. 81 bool contains(Object object); 82 /// Retrieve session factory used to create this session 83 SessionFactory getSessionFactory(); 84 /// Lookup metadata to find entity name for object. 85 string getEntityName(Object object); 86 /// Lookup metadata to find entity name for class type info. 87 string getEntityName(TypeInfo_Class type); 88 89 /// Return the persistent instance of the given named entity with the given identifier, or null if there is no such persistent instance. 90 T get(T : Object, ID)(ID id) 91 { 92 Variant v = id; 93 return cast(T) getObject(getEntityName(T.classinfo), v); 94 } 95 96 /// Read the persistent state associated with the given identifier into the given transient instance. 97 T load(T : Object, ID)(ID id) 98 { 99 Variant v = id; 100 return cast(T) loadObject(getEntityName(T.classinfo), v); 101 } 102 103 /// Read the persistent state associated with the given identifier into the given transient instance. 104 void load(T : Object, ID)(T obj, ID id) 105 { 106 Variant v = id; 107 loadObject(obj, v); 108 } 109 110 /// Return the persistent instance of the given named entity with the given identifier, or null if there is no such persistent instance. 111 Object getObject(string entityName, Variant id); 112 113 /// Read the persistent state associated with the given identifier into the given transient instance. 114 Object loadObject(string entityName, Variant id); 115 116 /// Read the persistent state associated with the given identifier into the given transient instance 117 void loadObject(Object obj, Variant id); 118 119 /// Re-read the state of the given instance from the underlying database. 120 void refresh(Object obj); 121 122 /// Persist the given transient instance, first assigning a generated identifier. 123 Variant save(Object obj); 124 125 /// Persist the given transient instance. 126 void persist(Object obj); 127 128 /// Update the persistent instance with the identifier of the given detached instance. 129 void update(Object object); 130 131 /// remove object from DB (renamed from original Session.delete - it's keyword in D) 132 void remove(Object object); 133 134 /// Create a new instance of Query for the given HQL query string 135 Query createQuery(string queryString); 136 } 137 138 /// Transaction interface: TODO 139 interface Transaction 140 { 141 } 142 143 /// Interface for usage of HQL queries. 144 abstract class Query 145 { 146 ///Get the query string. 147 string getQueryString(); 148 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 149 Object uniqueObject(); 150 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer. 151 Object uniqueObject(Object obj); 152 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 153 T uniqueResult(T : Object)() 154 { 155 return cast(T) uniqueObject(); 156 } 157 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer. 158 T uniqueResult(T : Object)(T obj) 159 { 160 return cast(T) uniqueObject(obj); 161 } 162 163 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 164 Variant[] uniqueRow(); 165 /// Return the query results as a List of entity objects 166 Object[] listObjects(); 167 /// Return the query results as a List of entity objects 168 T[] list(T : Object)() 169 { 170 return cast(T[]) listObjects(); 171 } 172 /// Return the query results as a List which each row as Variant array 173 Variant[][] listRows(); 174 175 /// Bind a value to a named query parameter (all :parameters used in query should be bound before executing query). 176 protected Query setParameterVariant(string name, Variant val); 177 178 /// Bind a value to a named query parameter (all :parameters used in query should be bound before executing query). 179 Query setParameter(T)(string name, T val) 180 { 181 static if (is(T == Variant)) 182 { 183 return setParameterVariant(name, val); 184 } 185 else 186 { 187 return setParameterVariant(name, Variant(val)); 188 } 189 } 190 } 191 192 /// Allows reaction to basic SessionFactory occurrences 193 interface SessionFactoryObserver 194 { 195 ///Callback to indicate that the given factory has been closed. 196 void sessionFactoryClosed(SessionFactory factory); 197 ///Callback to indicate that the given factory has been created and is now ready for use. 198 void sessionFactoryCreated(SessionFactory factory); 199 } 200 201 interface EventListeners 202 { 203 // TODO: 204 } 205 206 interface ConnectionProvider 207 { 208 } 209 210 interface Settings 211 { 212 Dialect getDialect(); 213 ConnectionProvider getConnectionProvider(); 214 bool isAutoCreateSchema(); 215 } 216 217 interface Mapping 218 { 219 string getIdentifierPropertyName(string className); 220 Type getIdentifierType(string className); 221 Type getReferencedPropertyType(string className, string propertyName); 222 } 223 224 class Configuration 225 { 226 bool dummy; 227 } 228 229 class EntityCache 230 { 231 const string name; 232 const EntityInfo entity; 233 Object[Variant] items; 234 this(const EntityInfo entity) 235 { 236 this.entity = entity; 237 this.name = entity.name; 238 } 239 240 bool hasKey(Variant key) 241 { 242 return ((key in items) !is null); 243 } 244 245 Object peek(Variant key) 246 { 247 if ((key in items) is null) 248 return null; 249 return items[key]; 250 } 251 252 Object get(Variant key) 253 { 254 enforceHelper!CacheException((key in items) !is null, 255 "entity " ~ name ~ " with key " ~ key.toString() ~ " not found in cache"); 256 return items[key]; 257 } 258 259 void put(Variant key, Object obj) 260 { 261 items[key] = obj; 262 } 263 264 Object[] lookup(Variant[] keys, out Variant[] unknownKeys) 265 { 266 Variant[] unknown; 267 Object[] res; 268 foreach (key; keys) 269 { 270 Object obj = peek(normalize(key)); 271 if (obj !is null) 272 { 273 res ~= obj; 274 } 275 else 276 { 277 unknown ~= normalize(key); 278 } 279 } 280 unknownKeys = unknown; 281 return res; 282 } 283 } 284 285 /// helper class to disconnect Lazy loaders from closed session. 286 class SessionAccessor 287 { 288 private SessionImpl _session; 289 290 this(SessionImpl session) 291 { 292 _session = session; 293 } 294 /// returns session, with session state check - throws LazyInitializationException if attempting to get unfetched lazy data while session is closed 295 SessionImpl get() 296 { 297 enforceHelper!LazyInitializationException(_session !is null, 298 "Cannot read from closed session"); 299 return _session; 300 } 301 /// nulls session reference 302 void onSessionClosed() 303 { 304 _session = null; 305 } 306 } 307 308 /// Implementation of DStruct session 309 class SessionImpl : Session 310 { 311 312 private bool closed; 313 SessionFactoryImpl sessionFactory; 314 private EntityMetaData metaData; 315 Dialect dialect; 316 DataSource connectionPool; 317 Connection conn; 318 319 EntityCache[string] cache; 320 321 private SessionAccessor _accessor; 322 @property SessionAccessor accessor() 323 { 324 return _accessor; 325 } 326 327 package EntityCache getCache(string entityName) 328 { 329 EntityCache res; 330 if ((entityName in cache) is null) 331 { 332 res = new EntityCache(metaData[entityName]); 333 cache[entityName] = res; 334 } 335 else 336 { 337 res = cache[entityName]; 338 } 339 return res; 340 } 341 342 package bool keyInCache(string entityName, Variant key) 343 { 344 return getCache(entityName).hasKey(normalize(key)); 345 } 346 347 package Object peekFromCache(string entityName, Variant key) 348 { 349 return getCache(entityName).peek(normalize(key)); 350 } 351 352 package Object getFromCache(string entityName, Variant key) 353 { 354 return getCache(entityName).get(normalize(key)); 355 } 356 357 package void putToCache(string entityName, Variant key, Object value) 358 { 359 return getCache(entityName).put(normalize(key), value); 360 } 361 362 package Object[] lookupCache(string entityName, Variant[] keys, out Variant[] unknownKeys) 363 { 364 return getCache(entityName).lookup(keys, unknownKeys); 365 } 366 367 override EntityMetaData getMetaData() 368 { 369 return metaData; 370 } 371 372 private void checkClosed() 373 { 374 enforceHelper!SessionException(!closed, "Session is closed"); 375 } 376 377 this(SessionFactoryImpl sessionFactory, EntityMetaData metaData, 378 Dialect dialect, DataSource connectionPool) 379 { 380 //writeln("Creating session"); 381 this.sessionFactory = sessionFactory; 382 this.metaData = metaData; 383 this.dialect = dialect; 384 this.connectionPool = connectionPool; 385 this.conn = connectionPool.getConnection(); 386 this._accessor = new SessionAccessor(this); 387 } 388 389 override Transaction beginTransaction() 390 { 391 throw new DStructException("Method not implemented"); 392 } 393 394 override void cancelQuery() 395 { 396 throw new DStructException("Method not implemented"); 397 } 398 399 override void clear() 400 { 401 throw new DStructException("Method not implemented"); 402 } 403 ///Does this session contain any changes which must be synchronized with the database? In other words, would any DML operations be executed if we flushed this session? 404 override bool isDirty() 405 { 406 throw new DStructException("Method not implemented"); 407 } 408 /// Check if the session is still open. 409 override bool isOpen() 410 { 411 return !closed; 412 } 413 /// Check if the session is currently connected. 414 override bool isConnected() 415 { 416 return !closed; 417 } 418 /// End the session by releasing the JDBC connection and cleaning up. 419 override Connection close() 420 { 421 checkClosed(); 422 _accessor.onSessionClosed(); 423 closed = true; 424 sessionFactory.sessionClosed(this); 425 //writeln("closing connection"); 426 assert(conn !is null); 427 conn.close(); 428 return null; 429 } 430 ///Check if this instance is associated with this Session 431 override bool contains(Object object) 432 { 433 throw new DStructException("Method not implemented"); 434 } 435 436 override SessionFactory getSessionFactory() 437 { 438 checkClosed(); 439 return sessionFactory; 440 } 441 442 override string getEntityName(Object object) 443 { 444 checkClosed(); 445 return metaData.findEntityForObject(object).name; 446 } 447 448 override string getEntityName(TypeInfo_Class type) 449 { 450 checkClosed(); 451 return metaData.getEntityName(type); 452 } 453 454 override Object getObject(string entityName, Variant id) 455 { 456 auto info = metaData.findEntity(entityName); 457 return getObject(info, null, id); 458 } 459 460 /// Read the persistent state associated with the given identifier into the given transient instance. 461 override Object loadObject(string entityName, Variant id) 462 { 463 Object obj = getObject(entityName, id); 464 enforceHelper!ObjectNotFoundException(obj !is null, 465 "Entity " ~ entityName ~ " with id " ~ to!string(id) ~ " not found"); 466 return obj; 467 } 468 469 /// Read the persistent state associated with the given identifier into the given transient instance 470 override void loadObject(Object obj, Variant id) 471 { 472 auto info = metaData.findEntityForObject(obj); 473 Object found = getObject(info, obj, id); 474 enforceHelper!ObjectNotFoundException(found !is null, 475 "Entity " ~ info.name ~ " with id " ~ to!string(id) ~ " not found"); 476 } 477 478 /// Read the persistent state associated with the given identifier into the given transient instance 479 Object getObject(const EntityInfo info, Object obj, Variant id) 480 { 481 string hql = "FROM " ~ info.name ~ " WHERE " ~ info.getKeyProperty().propertyName ~ "=:Id"; 482 Query q = createQuery(hql).setParameter("Id", id); 483 Object res = q.uniqueResult(obj); 484 return res; 485 } 486 487 /// Read entities referenced by property 488 Object[] loadReferencedObjects(const EntityInfo info, string referencePropertyName, Variant fk) 489 { 490 string hql = "SELECT a2 FROM " ~ info.name ~ " AS a1 JOIN a1." ~ referencePropertyName 491 ~ " AS a2 WHERE a1." ~ info.getKeyProperty().propertyName ~ "=:Fk"; 492 Query q = createQuery(hql).setParameter("Fk", fk); 493 Object[] res = q.listObjects(); 494 return res; 495 } 496 497 /// Re-read the state of the given instance from the underlying database. 498 override void refresh(Object obj) 499 { 500 auto info = metaData.findEntityForObject(obj); 501 string query = metaData.generateFindByPkForEntity(dialect, info); 502 enforceHelper!TransientObjectException(info.isKeySet(obj), 503 "Cannot refresh entity " ~ info.name ~ ": no Id specified"); 504 Variant id = info.getKey(obj); 505 //writeln("Finder query: " ~ query); 506 PreparedStatement stmt = conn.prepareStatement(query); 507 scope (exit) 508 stmt.close(); 509 stmt.setVariant(1, id); 510 ResultSet rs = stmt.executeQuery(); 511 //writeln("returned rows: " ~ to!string(rs.getFetchSize())); 512 scope (exit) 513 rs.close(); 514 if (rs.next()) 515 { 516 //writeln("reading columns"); 517 metaData.readAllColumns(obj, rs, 1); 518 //writeln("value: " ~ obj.toString); 519 } 520 else 521 { 522 // not found! 523 enforceHelper!ObjectNotFoundException(false, 524 "Entity " ~ info.name ~ " with id " ~ to!string(id) ~ " not found"); 525 } 526 } 527 528 private void saveRelations(const EntityInfo ei, Object obj) 529 { 530 foreach (p; ei) 531 { 532 if (p.manyToMany) 533 { 534 saveRelations(p, obj); 535 } 536 } 537 } 538 539 private void saveRelations(const PropertyInfo p, Object obj) 540 { 541 Object[] relations = p.getCollectionFunc(obj); 542 Variant thisId = p.entity.getKey(obj); 543 if (relations !is null && relations.length > 0) 544 { 545 string sql = p.joinTable.getInsertSQL(dialect); 546 string list; 547 foreach (r; relations) 548 { 549 Variant otherId = p.referencedEntity.getKey(r); 550 if (list.length != 0) 551 list ~= ", "; 552 list ~= createKeyPairSQL(thisId, otherId); 553 } 554 sql ~= list; 555 Statement stmt = conn.createStatement(); 556 scope (exit) 557 stmt.close(); 558 //writeln("sql: " ~ sql); 559 stmt.executeUpdate(sql); 560 } 561 } 562 563 private void updateRelations(const EntityInfo ei, Object obj) 564 { 565 foreach (p; ei) 566 { 567 if (p.manyToMany) 568 { 569 updateRelations(p, obj); 570 } 571 } 572 } 573 574 private void deleteRelations(const EntityInfo ei, Object obj) 575 { 576 foreach (p; ei) 577 { 578 if (p.manyToMany) 579 { 580 deleteRelations(p, obj); 581 } 582 } 583 } 584 585 private Variant[] readRelationIds(const PropertyInfo p, Variant thisId) 586 { 587 Variant[] res; 588 string q = p.joinTable.getOtherKeySelectSQL(dialect, createKeySQL(thisId)); 589 Statement stmt = conn.createStatement(); 590 scope (exit) 591 stmt.close(); 592 ResultSet rs = stmt.executeQuery(q); 593 scope (exit) 594 rs.close(); 595 while (rs.next()) 596 { 597 res ~= rs.getVariant(1); 598 } 599 return res; 600 } 601 602 private void updateRelations(const PropertyInfo p, Object obj) 603 { 604 Variant thisId = p.entity.getKey(obj); 605 Variant[] oldRelIds = readRelationIds(p, thisId); 606 Variant[] newRelIds = p.getCollectionIds(obj); 607 bool[string] oldmap; 608 foreach (v; oldRelIds) 609 oldmap[createKeySQL(v)] = true; 610 bool[string] newmap; 611 foreach (v; newRelIds) 612 newmap[createKeySQL(v)] = true; 613 string[] keysToDelete; 614 string[] keysToAdd; 615 foreach (v; newmap.keys) 616 if ((v in oldmap) is null) 617 keysToAdd ~= v; 618 foreach (v; oldmap.keys) 619 if ((v in newmap) is null) 620 keysToDelete ~= v; 621 if (keysToAdd.length > 0) 622 { 623 Statement stmt = conn.createStatement(); 624 scope (exit) 625 stmt.close(); 626 stmt.executeUpdate(p.joinTable.getInsertSQL(dialect, createKeySQL(thisId), keysToAdd)); 627 } 628 if (keysToDelete.length > 0) 629 { 630 Statement stmt = conn.createStatement(); 631 scope (exit) 632 stmt.close(); 633 stmt.executeUpdate(p.joinTable.getDeleteSQL(dialect, 634 createKeySQL(thisId), keysToDelete)); 635 } 636 } 637 638 private void deleteRelations(const PropertyInfo p, Object obj) 639 { 640 Variant thisId = p.entity.getKey(obj); 641 Variant[] oldRelIds = readRelationIds(p, thisId); 642 string[] ids; 643 foreach (v; oldRelIds) 644 ids ~= createKeySQL(v); 645 if (ids.length > 0) 646 { 647 Statement stmt = conn.createStatement(); 648 scope (exit) 649 stmt.close(); 650 stmt.executeUpdate(p.joinTable.getDeleteSQL(dialect, createKeySQL(thisId), ids)); 651 } 652 } 653 654 /// Persist the given transient instance, first assigning a generated identifier if not assigned; returns generated value 655 override Variant save(Object obj) 656 { 657 auto info = metaData.findEntityForObject(obj); 658 if (!info.isKeySet(obj)) 659 { 660 if (info.getKeyProperty().generated) 661 { 662 // autogenerated on DB side 663 string query = dialect.appendInsertToFetchGeneratedKey( 664 metaData.generateInsertNoKeyForEntity(dialect, info), info); 665 PreparedStatement stmt = conn.prepareStatement(query); 666 scope (exit) 667 stmt.close(); 668 metaData.writeAllColumns(obj, stmt, 1, true); 669 Variant generatedKey; 670 stmt.executeUpdate(generatedKey); 671 info.setKey(obj, generatedKey); 672 } 673 else if (info.getKeyProperty().generatorFunc !is null) 674 { 675 // has generator function 676 Variant generatedKey = info.getKeyProperty() 677 .generatorFunc(conn, info.getKeyProperty()); 678 info.setKey(obj, generatedKey); 679 string query = metaData.generateInsertAllFieldsForEntity(dialect, info); 680 PreparedStatement stmt = conn.prepareStatement(query); 681 scope (exit) 682 stmt.close(); 683 metaData.writeAllColumns(obj, stmt, 1); 684 stmt.executeUpdate(); 685 } 686 else 687 { 688 throw new PropertyValueException("Key is not set and no generator is specified"); 689 } 690 } 691 else 692 { 693 string query = metaData.generateInsertAllFieldsForEntity(dialect, info); 694 PreparedStatement stmt = conn.prepareStatement(query); 695 scope (exit) 696 stmt.close(); 697 metaData.writeAllColumns(obj, stmt, 1); 698 stmt.executeUpdate(); 699 } 700 Variant key = info.getKey(obj); 701 putToCache(info.name, key, obj); 702 saveRelations(info, obj); 703 return key; 704 } 705 706 /// Persist the given transient instance. 707 override void persist(Object obj) 708 { 709 auto info = metaData.findEntityForObject(obj); 710 enforceHelper!TransientObjectException(info.isKeySet(obj), 711 "Cannot persist entity w/o key assigned"); 712 string query = metaData.generateInsertAllFieldsForEntity(dialect, info); 713 PreparedStatement stmt = conn.prepareStatement(query); 714 scope (exit) 715 stmt.close(); 716 metaData.writeAllColumns(obj, stmt, 1); 717 stmt.executeUpdate(); 718 Variant key = info.getKey(obj); 719 putToCache(info.name, key, obj); 720 saveRelations(info, obj); 721 } 722 723 override void update(Object obj) 724 { 725 auto info = metaData.findEntityForObject(obj); 726 enforceHelper!TransientObjectException(info.isKeySet(obj), 727 "Cannot persist entity w/o key assigned"); 728 string query = metaData.generateUpdateForEntity(dialect, info); 729 //writeln("Query: " ~ query); 730 { 731 PreparedStatement stmt = conn.prepareStatement(query); 732 scope (exit) 733 stmt.close(); 734 const int columnCount = metaData.writeAllColumns(obj, stmt, 1, true); 735 info.keyProperty.writeFunc(obj, stmt, columnCount + 1); 736 stmt.executeUpdate(); 737 } 738 updateRelations(info, obj); 739 } 740 741 // renamed from Session.delete since delete is D keyword 742 override void remove(Object obj) 743 { 744 auto info = metaData.findEntityForObject(obj); 745 deleteRelations(info, obj); 746 string query = "DELETE FROM " ~ dialect.quoteIfNeeded( 747 info.tableName) ~ " WHERE " ~ dialect.quoteIfNeeded(info.getKeyProperty() 748 .columnName) ~ "=?"; 749 PreparedStatement stmt = conn.prepareStatement(query); 750 info.getKeyProperty().writeFunc(obj, stmt, 1); 751 stmt.executeUpdate(); 752 } 753 754 /// Create a new instance of Query for the given HQL query string 755 override Query createQuery(string queryString) 756 { 757 return new QueryImpl(this, queryString); 758 } 759 } 760 761 /// Implementation of DStruct SessionFactory 762 class SessionFactoryImpl : SessionFactory 763 { 764 // Configuration cfg; 765 // Mapping mapping; 766 // Settings settings; 767 // EventListeners listeners; 768 // SessionFactoryObserver observer; 769 private bool closed; 770 private EntityMetaData metaData; 771 Dialect dialect; 772 DataSource connectionPool; 773 774 SessionImpl[] activeSessions; 775 776 DBInfo _dbInfo; 777 override public DBInfo getDBMetaData() 778 { 779 if (_dbInfo is null) 780 _dbInfo = new DBInfo(dialect, metaData); 781 return _dbInfo; 782 } 783 784 void sessionClosed(SessionImpl session) 785 { 786 foreach (i, item; activeSessions) 787 { 788 if (item == session) 789 { 790 remove(activeSessions, i); 791 } 792 } 793 } 794 795 this(EntityMetaData metaData, Dialect dialect, DataSource connectionPool) 796 { 797 //writeln("Creating session factory"); 798 this.metaData = metaData; 799 this.dialect = dialect; 800 this.connectionPool = connectionPool; 801 } 802 803 // this(Configuration cfg, Mapping mapping, Settings settings, EventListeners listeners, SessionFactoryObserver observer) { 804 // this.cfg = cfg; 805 // this.mapping = mapping; 806 // this.settings = settings; 807 // this.listeners = listeners; 808 // this.observer = observer; 809 // if (observer !is null) 810 // observer.sessionFactoryCreated(this); 811 // } 812 private void checkClosed() 813 { 814 enforceHelper!SessionException(!closed, "Session factory is closed"); 815 } 816 817 override void close() 818 { 819 //writeln("Closing session factory"); 820 checkClosed(); 821 closed = true; 822 // if (observer !is null) 823 // observer.sessionFactoryClosed(this); 824 // TODO: 825 } 826 827 bool isClosed() 828 { 829 return closed; 830 } 831 832 Session openSession() 833 { 834 checkClosed(); 835 SessionImpl session = new SessionImpl(this, metaData, dialect, connectionPool); 836 activeSessions ~= session; 837 return session; 838 } 839 } 840 841 struct ObjectList 842 { 843 Object[] list; 844 void add(Object obj) 845 { 846 foreach (v; list) 847 { 848 if (v == obj) 849 { 850 return; // avoid duplicates 851 } 852 } 853 list ~= obj; 854 } 855 856 @property int length() 857 { 858 return cast(int) list.length; 859 } 860 861 ref Object opIndex(size_t index) 862 { 863 return list[index]; 864 } 865 } 866 867 Variant normalize(Variant v) 868 { 869 // variants of different type are not equal, normalize to make sure that keys Variant(1) and Variant(1L) are the same 870 if (v.convertsTo!long) 871 return Variant(v.get!long); 872 else if (v.convertsTo!ulong) 873 return Variant(v.get!ulong); 874 return Variant(v.toString()); 875 } 876 877 /// task to load reference entity 878 class PropertyLoadItem 879 { 880 const PropertyInfo property; 881 private ObjectList[Variant] _map; // ID to object list 882 this(const PropertyInfo property) 883 { 884 this.property = property; 885 } 886 887 @property ref ObjectList[Variant] map() 888 { 889 return _map; 890 } 891 892 @property Variant[] keys() 893 { 894 return _map.keys; 895 } 896 897 @property int length() 898 { 899 return cast(int) _map.length; 900 } 901 902 ObjectList* opIndex(Variant key) 903 { 904 Variant id = normalize(key); 905 if ((id in _map) is null) 906 { 907 _map[id] = ObjectList(); 908 } 909 //assert(length > 0); 910 return &_map[id]; 911 } 912 913 void add(ref Variant id, Object obj) 914 { 915 auto item = opIndex(id); 916 item.add(obj); 917 //assert(item.length == opIndex(id).length); 918 } 919 920 string createCommaSeparatedKeyList() 921 { 922 assert(map.length > 0); 923 return .createCommaSeparatedKeyList(map.keys); 924 } 925 } 926 927 string createKeySQL(Variant id) 928 { 929 if (id.convertsTo!long || id.convertsTo!ulong) 930 { 931 return id.toString(); 932 } 933 else 934 { 935 return "'" ~ id.toString() ~ "'"; 936 } 937 } 938 939 string createKeyPairSQL(Variant id1, Variant id2) 940 { 941 return "(" ~ createKeySQL(id1) ~ ", " ~ createKeySQL(id2) ~ ")"; 942 } 943 944 string createCommaSeparatedKeyList(Variant[] list) 945 { 946 assert(list.length > 0); 947 string res; 948 foreach (v; list) 949 { 950 if (res.length > 0) 951 res ~= ", "; 952 res ~= createKeySQL(v); 953 } 954 return res; 955 } 956 957 /// task to load reference entity 958 class EntityCollections 959 { 960 private ObjectList[Variant] _map; // ID to object list 961 @property ref ObjectList[Variant] map() 962 { 963 return _map; 964 } 965 966 @property Variant[] keys() 967 { 968 return _map.keys; 969 } 970 971 @property int length() 972 { 973 return cast(int) _map.length; 974 } 975 976 ref Object[] opIndex(Variant key) 977 { 978 //writeln("searching for key " ~ key.toString); 979 Variant id = normalize(key); 980 if ((id in _map) is null) 981 { 982 //writeln("creating new item"); 983 _map[id] = ObjectList(); 984 } 985 //assert(length > 0); 986 //writeln("returning item"); 987 return _map[id].list; 988 } 989 990 void add(ref Variant id, Object obj) 991 { 992 auto item = opIndex(id); 993 //writeln("item count = " ~ to!string(item.length)); 994 item ~= obj; 995 } 996 } 997 998 class PropertyLoadMap 999 { 1000 private PropertyLoadItem[const PropertyInfo] _map; 1001 PropertyLoadItem opIndex(const PropertyInfo prop) 1002 { 1003 if ((prop in _map) is null) 1004 { 1005 _map[prop] = new PropertyLoadItem(prop); 1006 } 1007 assert(_map.length > 0); 1008 return _map[prop]; 1009 } 1010 1011 this() 1012 { 1013 } 1014 1015 this(PropertyLoadMap plm) 1016 { 1017 foreach (k; plm.keys) 1018 { 1019 auto pli = plm[k]; 1020 foreach (plik; pli.map.keys) 1021 { 1022 foreach (obj; pli.map[plik].list.dup) 1023 { 1024 add(k, plik, obj); 1025 } 1026 } 1027 } 1028 } 1029 1030 PropertyLoadItem remove(const PropertyInfo pi) 1031 { 1032 PropertyLoadItem item = _map[pi]; 1033 _map.remove(pi); 1034 return item; 1035 } 1036 1037 @property ref PropertyLoadItem[const PropertyInfo] map() 1038 { 1039 return _map; 1040 } 1041 1042 @property int length() 1043 { 1044 return cast(int) _map.length; 1045 } 1046 1047 @property const(PropertyInfo)[] keys() 1048 { 1049 return _map.keys; 1050 } 1051 1052 final void add(const PropertyInfo property, Variant id, Object obj) 1053 { 1054 auto item = opIndex(property); 1055 item.add(id, obj); 1056 } 1057 } 1058 1059 class QueryImpl : Query 1060 { 1061 SessionImpl sess; 1062 ParsedQuery query; 1063 ParameterValues params; 1064 this(SessionImpl sess, string queryString) 1065 { 1066 this.sess = sess; 1067 //writeln("QueryImpl(): HQL: " ~ queryString); 1068 QueryParser parser = new QueryParser(sess.metaData, queryString); 1069 //writeln("parsing"); 1070 this.query = parser.makeSQL(sess.dialect); 1071 //writeln("SQL: " ~ this.query.sql); 1072 params = query.createParams(); 1073 //writeln("exiting QueryImpl()"); 1074 } 1075 1076 override string getQueryString() 1077 { 1078 return query.hql; 1079 } 1080 1081 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 1082 override Object uniqueObject() 1083 { 1084 return uniqueResult(cast(Object) null); 1085 } 1086 1087 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer. 1088 override Object uniqueObject(Object obj) 1089 { 1090 Object[] rows = listObjects(obj); 1091 if (rows == null) 1092 return null; 1093 enforceHelper!NonUniqueResultException(rows.length == 1, 1094 "Query returned more than one object: " ~ getQueryString()); 1095 return rows[0]; 1096 } 1097 1098 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 1099 override Variant[] uniqueRow() 1100 { 1101 Variant[][] rows = listRows(); 1102 if (rows == null) 1103 return null; 1104 enforceHelper!NonUniqueResultException(rows.length == 1, 1105 "Query returned more than one row: " ~ getQueryString()); 1106 return rows[0]; 1107 } 1108 1109 private FromClauseItem findRelation(FromClauseItem from, const PropertyInfo prop) 1110 { 1111 for (int i = 0; i < query.from.length; i++) 1112 { 1113 FromClauseItem f = query.from[i]; 1114 if (f.base == from && f.baseProperty == prop) 1115 return f; 1116 if (f.entity == prop.referencedEntity && from.base == f) 1117 return f; 1118 } 1119 return null; 1120 } 1121 1122 private Object readRelations(Object objectContainer, DataSetReader r, PropertyLoadMap loadMap) 1123 { 1124 Object[] relations = new Object[query.select.length]; 1125 //writeln("select clause len = " ~ to!string(query.select.length)); 1126 // read all related objects from DB row 1127 for (int i = 0; i < query.select.length; i++) 1128 { 1129 FromClauseItem from = query.select[i].from; 1130 //writeln("reading " ~ from.entityName); 1131 Object row; 1132 if (!from.entity.isKeyNull(r, from.startColumn)) 1133 { 1134 //writeln("key is not null"); 1135 Variant key = from.entity.getKey(r, from.startColumn); 1136 //writeln("key is " ~ key.toString); 1137 row = sess.peekFromCache(from.entity.name, key); 1138 if (row is null) 1139 { 1140 //writeln("row not found in cache"); 1141 row = (objectContainer !is null && i == 0) ? objectContainer : from.entity.createEntity(); 1142 //writeln("reading all columns"); 1143 sess.metaData.readAllColumns(row, r, from.startColumn); 1144 sess.putToCache(from.entity.name, key, row); 1145 } 1146 else if (objectContainer !is null) 1147 { 1148 //writeln("copying all properties to existing container"); 1149 from.entity.copyAllProperties(objectContainer, row); 1150 } 1151 } 1152 relations[i] = row; 1153 } 1154 //writeln("fill relations..."); 1155 // fill relations 1156 for (int i = 0; i < query.select.length; i++) 1157 { 1158 if (relations[i] is null) 1159 continue; 1160 FromClauseItem from = query.select[i].from; 1161 auto ei = from.entity; 1162 for (int j = 0; j < ei.length; j++) 1163 { 1164 auto pi = ei[j]; 1165 if (pi.oneToOne || pi.manyToOne) 1166 { 1167 static if (TRACE_REFS) 1168 writeln("updating relations for " ~ from.pathString ~ "." ~ pi.propertyName); 1169 FromClauseItem rfrom = findRelation(from, pi); 1170 if (rfrom !is null && rfrom.selectIndex >= 0) 1171 { 1172 Object rel = relations[rfrom.selectIndex]; 1173 pi.setObjectFunc(relations[i], rel); 1174 } 1175 else 1176 { 1177 if (pi.columnName !is null) 1178 { 1179 static if (TRACE_REFS) 1180 writeln("relation " ~ pi.propertyName ~ " has column name"); 1181 if (r.isNull(from.startColumn + pi.columnOffset)) 1182 { 1183 // FK is null, set NULL to field 1184 pi.setObjectFunc(relations[i], null); 1185 static if (TRACE_REFS) 1186 writeln("relation " ~ pi.propertyName ~ " has null FK"); 1187 } 1188 else 1189 { 1190 Variant id = r.getVariant(from.startColumn + pi.columnOffset); 1191 Object existing = sess.peekFromCache(pi.referencedEntity.name, id); 1192 if (existing !is null) 1193 { 1194 static if (TRACE_REFS) 1195 writeln("existing relation found in cache"); 1196 pi.setObjectFunc(relations[i], existing); 1197 } 1198 else 1199 { 1200 // FK is not null 1201 if (pi.lazyLoad) 1202 { 1203 // lazy load 1204 static if (TRACE_REFS) 1205 writeln("scheduling lazy load for " ~ from.pathString ~ "." 1206 ~ pi.propertyName ~ " with FK " ~ id.toString); 1207 LazyObjectLoader loader = new LazyObjectLoader(sess.accessor, 1208 pi, id); 1209 pi.setObjectDelegateFunc(relations[i], &loader.load); 1210 } 1211 else 1212 { 1213 // delayed load 1214 static if (TRACE_REFS) 1215 writeln("relation " ~ pi.propertyName ~ " with FK " ~ id.toString() 1216 ~ " will be loaded later"); 1217 loadMap.add(pi, id, relations[i]); // to load later 1218 } 1219 } 1220 } 1221 } 1222 else 1223 { 1224 // TODO: 1225 assert(false, 1226 "relation " ~ pi.propertyName 1227 ~ " has no column name. To be implemented."); 1228 } 1229 } 1230 } 1231 else if (pi.oneToMany || pi.manyToMany) 1232 { 1233 Variant id = ei.getKey(relations[i]); 1234 if (pi.lazyLoad) 1235 { 1236 // lazy load 1237 static if (TRACE_REFS) 1238 writeln("creating lazy loader for " ~ from.pathString 1239 ~ "." ~ pi.propertyName ~ " by FK " ~ id.toString); 1240 LazyCollectionLoader loader = new LazyCollectionLoader(sess.accessor, 1241 pi, id); 1242 pi.setCollectionDelegateFunc(relations[i], &loader.load); 1243 } 1244 else 1245 { 1246 // delayed load 1247 static if (TRACE_REFS) 1248 writeln("Relation " ~ from.pathString ~ "." ~ pi.propertyName 1249 ~ " will be loaded later by FK " ~ id.toString); 1250 loadMap.add(pi, id, relations[i]); // to load later 1251 } 1252 } 1253 } 1254 } 1255 return relations[0]; 1256 } 1257 1258 /// Return the query results as a List of entity objects 1259 override Object[] listObjects() 1260 { 1261 return listObjects(null); 1262 } 1263 1264 private void delayedLoadRelations(PropertyLoadMap loadMap) 1265 { 1266 loadMap = new PropertyLoadMap(loadMap); 1267 1268 auto types = loadMap.keys; 1269 static if (TRACE_REFS) 1270 writeln("delayedLoadRelations " ~ to!string(loadMap.length)); 1271 1272 foreach (pi; types) 1273 { 1274 static if (TRACE_REFS) 1275 writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName); 1276 assert(pi.referencedEntity !is null); 1277 auto map = loadMap.remove(pi); 1278 if (map.length == 0) 1279 continue; 1280 //writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName); 1281 string keys = map.createCommaSeparatedKeyList(); 1282 if (pi.oneToOne || pi.manyToOne) 1283 { 1284 if (pi.columnName !is null) 1285 { 1286 Variant[] unknownKeys; 1287 Object[] list = sess.lookupCache(pi.referencedEntity.name, 1288 map.keys, unknownKeys); 1289 if (unknownKeys.length > 0) 1290 { 1291 string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " 1292 ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ createCommaSeparatedKeyList( 1293 unknownKeys) ~ ")"; 1294 static if (TRACE_REFS) 1295 writeln( 1296 "delayedLoadRelations: loading " 1297 ~ pi.propertyName ~ " HQL: " ~ hql); 1298 QueryImpl q = cast(QueryImpl) sess.createQuery(hql); 1299 Object[] fromDB = q.listObjects(null, loadMap); 1300 list ~= fromDB; 1301 static if (TRACE_REFS) 1302 writeln("delayedLoadRelations: objects loaded " ~ to!string( 1303 fromDB.length)); 1304 } 1305 else 1306 { 1307 static if (TRACE_REFS) 1308 writeln("all objects found in cache"); 1309 } 1310 static if (TRACE_REFS) 1311 writeln("delayedLoadRelations: updating"); 1312 foreach (rel; list) 1313 { 1314 static if (TRACE_REFS) 1315 writeln( 1316 "delayedLoadRelations: reading key from " 1317 ~ pi.referencedEntity.name); 1318 Variant key = pi.referencedEntity.getKey(rel); 1319 //writeln("map length before: " ~ to!string(map.length)); 1320 auto objectsToUpdate = map[key].list; 1321 //writeln("map length after: " ~ to!string(map.length)); 1322 //writeln("updating relations with key " ~ key.toString() ~ " (" ~ to!string(objectsToUpdate.length) ~ ")"); 1323 foreach (obj; objectsToUpdate) 1324 { 1325 pi.setObjectFunc(obj, rel); 1326 } 1327 } 1328 } 1329 else 1330 { 1331 assert(false, 1332 "Delayed loader for non-join column is not yet implemented for OneToOne and ManyToOne"); 1333 } 1334 } 1335 else if (pi.oneToMany || pi.manyToMany) 1336 { 1337 string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " ~ pi.referencedPropertyName ~ "." 1338 ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ keys ~ ")"; 1339 static if (TRACE_REFS) 1340 writeln("delayedLoadRelations: loading " ~ pi.propertyName ~ " HQL: " ~ hql); 1341 QueryImpl q = cast(QueryImpl) sess.createQuery(hql); 1342 assert(q !is null); 1343 Object[] list = q.listObjects(null, loadMap); 1344 static if (TRACE_REFS) 1345 writeln("delayedLoadRelations oneToMany/manyToMany: objects loaded " ~ to!string( 1346 list.length)); 1347 EntityCollections collections = new EntityCollections(); 1348 // group by referenced PK 1349 foreach (rel; list) 1350 { 1351 static if (TRACE_REFS) 1352 writeln("delayedLoadRelations oneToMany/manyToMany: reading reference from " 1353 ~ pi.referencedEntity.name ~ "." ~ pi.referencedProperty.propertyName 1354 ~ " joinColumn=" ~ pi.referencedProperty.columnName); 1355 assert(pi.referencedProperty.manyToOne, 1356 "property referenced from OneToMany should be ManyToOne"); 1357 assert(pi.referencedProperty.getObjectFunc !is null); 1358 assert(rel !is null); 1359 //writeln("delayedLoadRelations oneToMany: reading object " ~ rel.classinfo.toString); 1360 Object obj = pi.referencedProperty.getObjectFunc(rel); 1361 //writeln("delayedLoadRelations oneToMany: object is read"); 1362 if (obj !is null) 1363 { 1364 //writeln("delayedLoadRelations oneToMany: object is not null"); 1365 //writeln("pi.entity.name=" ~ pi.entity.name ~ ", obj is " ~ obj.classinfo.toString); 1366 //writeln("obj = " ~ obj.toString); 1367 //writeln("pi.entity.keyProperty=" ~ pi.entity.keyProperty.propertyName); 1368 //assert(pi.entity.keyProperty.getFunc !is null); 1369 //Variant k = pi.entity.keyProperty.getFunc(obj); 1370 //writeln("key=" ~ k.toString); 1371 Variant key = pi.entity.getKey(obj); 1372 collections[key] ~= rel; 1373 //collections.add(k, rel); 1374 } 1375 } 1376 // update objects 1377 foreach (key; collections.keys) 1378 { 1379 auto objectsToUpdate = map[key].list; 1380 foreach (obj; objectsToUpdate) 1381 { 1382 pi.setCollectionFunc(obj, collections[key]); 1383 } 1384 } 1385 } 1386 } 1387 } 1388 1389 /// Return the query results as a List of entity objects 1390 Object[] listObjects(Object placeFirstObjectHere) 1391 { 1392 PropertyLoadMap loadMap = new PropertyLoadMap(); 1393 return listObjects(placeFirstObjectHere, loadMap); 1394 } 1395 1396 /// Return the query results as a List of entity objects 1397 Object[] listObjects(Object placeFirstObjectHere, PropertyLoadMap loadMap) 1398 { 1399 static if (TRACE_REFS) 1400 writeln("Entering listObjects " ~ query.hql); 1401 auto ei = query.entity; 1402 enforceHelper!SessionException(ei !is null, 1403 "No entity expected in result of query " ~ getQueryString()); 1404 params.checkAllParametersSet(); 1405 sess.checkClosed(); 1406 1407 Object[] res; 1408 1409 PreparedStatement stmt = sess.conn.prepareStatement(query.sql); 1410 scope (exit) 1411 stmt.close(); 1412 params.applyParams(stmt); 1413 ResultSet rs = stmt.executeQuery(); 1414 assert(query.select !is null && query.select.length > 0); 1415 // const int startColumn = query.select[0].from.startColumn; 1416 { 1417 scope (exit) 1418 rs.close(); 1419 while (rs.next()) 1420 { 1421 Object row = readRelations(res.length > 0 ? null : placeFirstObjectHere, 1422 rs, loadMap); 1423 if (row !is null) 1424 res ~= row; 1425 } 1426 } 1427 if (loadMap.length > 0) 1428 { 1429 static if (TRACE_REFS) 1430 writeln("relation properties scheduled for load: loadMap.length == " ~ to!string( 1431 loadMap.length)); 1432 delayedLoadRelations(loadMap); 1433 } 1434 static if (TRACE_REFS) 1435 writeln("Exiting listObjects " ~ query.hql); 1436 return res.length > 0 ? res : null; 1437 } 1438 1439 /// Return the query results as a List which each row as Variant array 1440 override Variant[][] listRows() 1441 { 1442 params.checkAllParametersSet(); 1443 sess.checkClosed(); 1444 1445 Variant[][] res; 1446 1447 //writeln("SQL: " ~ query.sql); 1448 PreparedStatement stmt = sess.conn.prepareStatement(query.sql); 1449 scope (exit) 1450 stmt.close(); 1451 params.applyParams(stmt); 1452 ResultSet rs = stmt.executeQuery(); 1453 scope (exit) 1454 rs.close(); 1455 while (rs.next()) 1456 { 1457 Variant[] row = new Variant[query.colCount]; 1458 for (int i = 1; i <= query.colCount; i++) 1459 row[i - 1] = rs.getVariant(i); 1460 res ~= row; 1461 } 1462 return res.length > 0 ? res : null; 1463 } 1464 1465 /// Bind a value to a named query parameter. 1466 override protected Query setParameterVariant(string name, Variant val) 1467 { 1468 params.setParameter(name, val); 1469 return this; 1470 } 1471 } 1472 1473 class LazyObjectLoader 1474 { 1475 const PropertyInfo pi; 1476 Variant id; 1477 SessionAccessor sess; 1478 this(SessionAccessor sess, const PropertyInfo pi, Variant id) 1479 { 1480 static if (TRACE_REFS) 1481 writeln("Created lazy loader for " ~ pi.referencedEntityName ~ " with id " ~ id 1482 .toString); 1483 this.pi = pi; 1484 this.id = id; 1485 this.sess = sess; 1486 } 1487 1488 Object load() 1489 { 1490 static if (TRACE_REFS) 1491 writeln("LazyObjectLoader.load()"); 1492 static if (TRACE_REFS) 1493 writeln("lazy loading of " ~ pi.referencedEntityName ~ " with id " ~ id.toString); 1494 return sess.get().loadObject(pi.referencedEntityName, id); 1495 } 1496 } 1497 1498 class LazyCollectionLoader 1499 { 1500 const PropertyInfo pi; 1501 Variant fk; 1502 SessionAccessor sess; 1503 this(SessionAccessor sess, const PropertyInfo pi, Variant fk) 1504 { 1505 assert(!pi.oneToMany || (pi.referencedEntity !is null && pi.referencedProperty !is null), 1506 "LazyCollectionLoader: No referenced property specified for OneToMany foreign key column"); 1507 static if (TRACE_REFS) 1508 writeln("Created lazy loader for collection for references " 1509 ~ pi.entity.name ~ "." ~ pi.propertyName ~ " by id " ~ fk.toString); 1510 this.pi = pi; 1511 this.fk = fk; 1512 this.sess = sess; 1513 } 1514 1515 Object[] load() 1516 { 1517 static if (TRACE_REFS) 1518 writeln("LazyObjectLoader.load()"); 1519 static if (TRACE_REFS) 1520 writeln( 1521 "lazy loading of references " ~ pi.entity.name ~ "." 1522 ~ pi.propertyName ~ " by id " ~ fk.toString); 1523 Object[] res = sess.get().loadReferencedObjects(pi.entity, pi.propertyName, fk); 1524 return res; 1525 } 1526 }