1 /** 2 * HibernateD - 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 hibernated/session.d. 8 * 9 * This module contains implementation of Hibernated 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 hibernated.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 ddbc.core; 24 import ddbc.common; 25 26 import hibernated.type; 27 import hibernated.dialect; 28 import hibernated.core; 29 import hibernated.metadata; 30 import hibernated.query; 31 32 // For backwards compatibily 33 // 'enforceEx' will be removed with 2.089 34 static if(__VERSION__ < 2080) { 35 alias enforceHelper = enforceEx; 36 } else { 37 alias enforceHelper = enforce; 38 } 39 40 41 const TRACE_REFS = false; 42 43 /// Factory to create HibernateD Sessions - similar to org.hibernate.SessionFactory 44 interface SessionFactory { 45 /// close all active sessions 46 void close(); 47 /// check if session factory is closed 48 bool isClosed(); 49 /// creates new session 50 Session openSession(); 51 /// retrieve information about tables and indexes for schema 52 DBInfo getDBMetaData(); 53 } 54 55 /// Session - main interface to load and persist entities -- similar to org.hibernate.Session 56 abstract class Session 57 { 58 /// returns metadata 59 EntityMetaData getMetaData(); 60 61 /// not supported in current implementation 62 Transaction beginTransaction(); 63 /// not supported in current implementation 64 void cancelQuery(); 65 /// not supported in current implementation 66 void clear(); 67 68 /// closes session 69 Connection close(); 70 71 ///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? 72 bool isDirty(); 73 /// Check if the session is still open. 74 bool isOpen(); 75 /// Check if the session is currently connected. 76 bool isConnected(); 77 /// Check if this instance is associated with this Session. 78 bool contains(Object object); 79 /// Retrieve session factory used to create this session 80 SessionFactory getSessionFactory(); 81 /// Lookup metadata to find entity name for object. 82 string getEntityName(Object object); 83 /// Lookup metadata to find entity name for class type info. 84 string getEntityName(TypeInfo_Class type); 85 86 /// Return the persistent instance of the given named entity with the given identifier, or null if there is no such persistent instance. 87 T get(T : Object, ID)(ID id) { 88 Variant v = id; 89 return cast(T)getObject(getEntityName(T.classinfo), v); 90 } 91 92 /// Read the persistent state associated with the given identifier into the given transient instance. 93 T load(T : Object, ID)(ID id) { 94 Variant v = id; 95 return cast(T)loadObject(getEntityName(T.classinfo), v); 96 } 97 98 /// Read the persistent state associated with the given identifier into the given transient instance. 99 void load(T : Object, ID)(T obj, ID id) { 100 Variant v = id; 101 loadObject(obj, v); 102 } 103 104 /// Return the persistent instance of the given named entity with the given identifier, or null if there is no such persistent instance. 105 Object getObject(string entityName, Variant id); 106 107 /// Read the persistent state associated with the given identifier into the given transient instance. 108 Object loadObject(string entityName, Variant id); 109 110 /// Read the persistent state associated with the given identifier into the given transient instance 111 void loadObject(Object obj, Variant id); 112 113 /// Re-read the state of the given instance from the underlying database. 114 void refresh(Object obj); 115 116 /// Persist the given transient instance, first assigning a generated identifier. 117 Variant save(Object obj); 118 119 /// Persist the given transient instance. 120 void persist(Object obj); 121 122 /// Update the persistent instance with the identifier of the given detached instance. 123 void update(Object object); 124 125 /// remove object from DB (renamed from original Session.delete - it's keyword in D) 126 void remove(Object object); 127 128 /// Create a new instance of Query for the given HQL query string 129 Query createQuery(string queryString); 130 } 131 132 /// Transaction interface: TODO 133 interface Transaction { 134 } 135 136 /// Interface for usage of HQL queries. 137 abstract class Query 138 { 139 ///Get the query string. 140 string getQueryString(); 141 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 142 Object uniqueObject(); 143 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer. 144 Object uniqueObject(Object obj); 145 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 146 T uniqueResult(T : Object)() { 147 return cast(T)uniqueObject(); 148 } 149 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer. 150 T uniqueResult(T : Object)(T obj) { 151 return cast(T)uniqueObject(obj); 152 } 153 154 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 155 Variant[] uniqueRow(); 156 /// Return the query results as a List of entity objects 157 Object[] listObjects(); 158 /// Return the query results as a List of entity objects 159 T[] list(T : Object)() { 160 return cast(T[])listObjects(); 161 } 162 /// Return the query results as a List which each row as Variant array 163 Variant[][] listRows(); 164 165 /// Bind a value to a named query parameter (all :parameters used in query should be bound before executing query). 166 protected Query setParameterVariant(string name, Variant val); 167 168 /// Bind a value to a named query parameter (all :parameters used in query should be bound before executing query). 169 Query setParameter(T)(string name, T val) { 170 static if (is(T == Variant)) { 171 return setParameterVariant(name, val); 172 } else { 173 return setParameterVariant(name, Variant(val)); 174 } 175 } 176 } 177 178 179 /// Allows reaction to basic SessionFactory occurrences 180 interface SessionFactoryObserver { 181 ///Callback to indicate that the given factory has been closed. 182 void sessionFactoryClosed(SessionFactory factory); 183 ///Callback to indicate that the given factory has been created and is now ready for use. 184 void sessionFactoryCreated(SessionFactory factory); 185 } 186 187 interface EventListeners { 188 // TODO: 189 } 190 191 interface ConnectionProvider { 192 } 193 194 interface Settings { 195 Dialect getDialect(); 196 ConnectionProvider getConnectionProvider(); 197 bool isAutoCreateSchema(); 198 } 199 200 interface Mapping { 201 string getIdentifierPropertyName(string className); 202 Type getIdentifierType(string className); 203 Type getReferencedPropertyType(string className, string propertyName); 204 } 205 206 class Configuration { 207 bool dummy; 208 } 209 210 class EntityCache { 211 const string name; 212 const EntityInfo entity; 213 Object[Variant] items; 214 this(const EntityInfo entity) { 215 this.entity = entity; 216 this.name = entity.name; 217 } 218 bool hasKey(Variant key) { 219 return ((key in items) !is null); 220 } 221 Object peek(Variant key) { 222 if ((key in items) is null) 223 return null; 224 return items[key]; 225 } 226 Object get(Variant key) { 227 enforceHelper!CacheException((key in items) !is null, "entity " ~ name ~ " with key " ~ key.toString() ~ " not found in cache"); 228 return items[key]; 229 } 230 void put(Variant key, Object obj) { 231 items[key] = obj; 232 } 233 Object[] lookup(Variant[] keys, out Variant[] unknownKeys) { 234 Variant[] unknown; 235 Object[] res; 236 foreach(key; keys) { 237 Object obj = peek(normalize(key)); 238 if (obj !is null) { 239 res ~= obj; 240 } else { 241 unknown ~= normalize(key); 242 } 243 } 244 unknownKeys = unknown; 245 return res; 246 } 247 } 248 249 /// helper class to disconnect Lazy loaders from closed session. 250 class SessionAccessor { 251 private SessionImpl _session; 252 253 this(SessionImpl session) { 254 _session = session; 255 } 256 /// returns session, with session state check - throws LazyInitializationException if attempting to get unfetched lazy data while session is closed 257 SessionImpl get() { 258 enforceHelper!LazyInitializationException(_session !is null, "Cannot read from closed session"); 259 return _session; 260 } 261 /// nulls session reference 262 void onSessionClosed() { 263 _session = null; 264 } 265 } 266 267 /// Implementation of HibernateD session 268 class SessionImpl : Session { 269 270 private bool closed; 271 SessionFactoryImpl sessionFactory; 272 private EntityMetaData metaData; 273 Dialect dialect; 274 DataSource connectionPool; 275 Connection conn; 276 277 EntityCache[string] cache; 278 279 private SessionAccessor _accessor; 280 @property SessionAccessor accessor() { 281 return _accessor; 282 } 283 284 package EntityCache getCache(string entityName) { 285 EntityCache res; 286 if ((entityName in cache) is null) { 287 res = new EntityCache(metaData[entityName]); 288 cache[entityName] = res; 289 } else { 290 res = cache[entityName]; 291 } 292 return res; 293 } 294 295 package bool keyInCache(string entityName, Variant key) { 296 return getCache(entityName).hasKey(normalize(key)); 297 } 298 299 package Object peekFromCache(string entityName, Variant key) { 300 return getCache(entityName).peek(normalize(key)); 301 } 302 303 package Object getFromCache(string entityName, Variant key) { 304 return getCache(entityName).get(normalize(key)); 305 } 306 307 package void putToCache(string entityName, Variant key, Object value) { 308 return getCache(entityName).put(normalize(key), value); 309 } 310 311 package Object[] lookupCache(string entityName, Variant[] keys, out Variant[] unknownKeys) { 312 return getCache(entityName).lookup(keys, unknownKeys); 313 } 314 315 override EntityMetaData getMetaData() { 316 return metaData; 317 } 318 319 private void checkClosed() { 320 enforceHelper!SessionException(!closed, "Session is closed"); 321 } 322 323 this(SessionFactoryImpl sessionFactory, EntityMetaData metaData, Dialect dialect, DataSource connectionPool) { 324 //writeln("Creating session"); 325 this.sessionFactory = sessionFactory; 326 this.metaData = metaData; 327 this.dialect = dialect; 328 this.connectionPool = connectionPool; 329 this.conn = connectionPool.getConnection(); 330 this._accessor = new SessionAccessor(this); 331 } 332 333 override Transaction beginTransaction() { 334 throw new HibernatedException("Method not implemented"); 335 } 336 override void cancelQuery() { 337 throw new HibernatedException("Method not implemented"); 338 } 339 override void clear() { 340 throw new HibernatedException("Method not implemented"); 341 } 342 ///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? 343 override bool isDirty() { 344 throw new HibernatedException("Method not implemented"); 345 } 346 /// Check if the session is still open. 347 override bool isOpen() { 348 return !closed; 349 } 350 /// Check if the session is currently connected. 351 override bool isConnected() { 352 return !closed; 353 } 354 /// End the session by releasing the JDBC connection and cleaning up. 355 override Connection close() { 356 checkClosed(); 357 _accessor.onSessionClosed(); 358 closed = true; 359 sessionFactory.sessionClosed(this); 360 //writeln("closing connection"); 361 assert(conn !is null); 362 conn.close(); 363 return null; 364 } 365 ///Check if this instance is associated with this Session 366 override bool contains(Object object) { 367 throw new HibernatedException("Method not implemented"); 368 } 369 override SessionFactory getSessionFactory() { 370 checkClosed(); 371 return sessionFactory; 372 } 373 374 override string getEntityName(Object object) { 375 checkClosed(); 376 return metaData.findEntityForObject(object).name; 377 } 378 379 override string getEntityName(TypeInfo_Class type) { 380 checkClosed(); 381 return metaData.getEntityName(type); 382 } 383 384 override Object getObject(string entityName, Variant id) { 385 auto info = metaData.findEntity(entityName); 386 return getObject(info, null, id); 387 } 388 389 /// Read the persistent state associated with the given identifier into the given transient instance. 390 override Object loadObject(string entityName, Variant id) { 391 Object obj = getObject(entityName, id); 392 enforceHelper!ObjectNotFoundException(obj !is null, "Entity " ~ entityName ~ " with id " ~ to!string(id) ~ " not found"); 393 return obj; 394 } 395 396 /// Read the persistent state associated with the given identifier into the given transient instance 397 override void loadObject(Object obj, Variant id) { 398 auto info = metaData.findEntityForObject(obj); 399 Object found = getObject(info, obj, id); 400 enforceHelper!ObjectNotFoundException(found !is null, "Entity " ~ info.name ~ " with id " ~ to!string(id) ~ " not found"); 401 } 402 403 /// Read the persistent state associated with the given identifier into the given transient instance 404 Object getObject(const EntityInfo info, Object obj, Variant id) { 405 string hql = "FROM " ~ info.name ~ " WHERE " ~ info.getKeyProperty().propertyName ~ "=:Id"; 406 Query q = createQuery(hql).setParameter("Id", id); 407 Object res = q.uniqueResult(obj); 408 return res; 409 } 410 411 /// Read entities referenced by property 412 Object[] loadReferencedObjects(const EntityInfo info, string referencePropertyName, Variant fk) { 413 string hql = "SELECT a2 FROM " ~ info.name ~ " AS a1 JOIN a1." ~ referencePropertyName ~ " AS a2 WHERE a1." ~ info.getKeyProperty().propertyName ~ "=:Fk"; 414 Query q = createQuery(hql).setParameter("Fk", fk); 415 Object[] res = q.listObjects(); 416 return res; 417 } 418 419 /// Re-read the state of the given instance from the underlying database. 420 override void refresh(Object obj) { 421 auto info = metaData.findEntityForObject(obj); 422 string query = metaData.generateFindByPkForEntity(dialect, info); 423 enforceHelper!TransientObjectException(info.isKeySet(obj), "Cannot refresh entity " ~ info.name ~ ": no Id specified"); 424 Variant id = info.getKey(obj); 425 //writeln("Finder query: " ~ query); 426 PreparedStatement stmt = conn.prepareStatement(query); 427 scope(exit) stmt.close(); 428 stmt.setVariant(1, id); 429 ResultSet rs = stmt.executeQuery(); 430 //writeln("returned rows: " ~ to!string(rs.getFetchSize())); 431 scope(exit) rs.close(); 432 if (rs.next()) { 433 //writeln("reading columns"); 434 metaData.readAllColumns(obj, rs, 1); 435 //writeln("value: " ~ obj.toString); 436 } else { 437 // not found! 438 enforceHelper!ObjectNotFoundException(false, "Entity " ~ info.name ~ " with id " ~ to!string(id) ~ " not found"); 439 } 440 } 441 442 private void saveRelations(const EntityInfo ei, Object obj) { 443 foreach(p; ei) { 444 if (p.manyToMany) { 445 saveRelations(p, obj); 446 } 447 } 448 } 449 450 private void saveRelations(const PropertyInfo p, Object obj) { 451 Object[] relations = p.getCollectionFunc(obj); 452 Variant thisId = p.entity.getKey(obj); 453 if (relations !is null && relations.length > 0) { 454 string sql = p.joinTable.getInsertSQL(dialect); 455 string list; 456 foreach(r; relations) { 457 Variant otherId = p.referencedEntity.getKey(r); 458 if (list.length != 0) 459 list ~= ", "; 460 list ~= createKeyPairSQL(thisId, otherId); 461 } 462 sql ~= list; 463 Statement stmt = conn.createStatement(); 464 scope(exit) stmt.close(); 465 //writeln("sql: " ~ sql); 466 stmt.executeUpdate(sql); 467 } 468 } 469 470 private void updateRelations(const EntityInfo ei, Object obj) { 471 foreach(p; ei) { 472 if (p.manyToMany) { 473 updateRelations(p, obj); 474 } 475 } 476 } 477 478 private void deleteRelations(const EntityInfo ei, Object obj) { 479 foreach(p; ei) { 480 if (p.manyToMany) { 481 deleteRelations(p, obj); 482 } 483 } 484 } 485 486 private Variant[] readRelationIds(const PropertyInfo p, Variant thisId) { 487 Variant[] res; 488 string q = p.joinTable.getOtherKeySelectSQL(dialect, createKeySQL(thisId)); 489 Statement stmt = conn.createStatement(); 490 scope(exit) stmt.close(); 491 ResultSet rs = stmt.executeQuery(q); 492 scope(exit) rs.close(); 493 while (rs.next()) { 494 res ~= rs.getVariant(1); 495 } 496 return res; 497 } 498 499 private void updateRelations(const PropertyInfo p, Object obj) { 500 Variant thisId = p.entity.getKey(obj); 501 Variant[] oldRelIds = readRelationIds(p, thisId); 502 Variant[] newRelIds = p.getCollectionIds(obj); 503 bool[string] oldmap; 504 foreach(v; oldRelIds) 505 oldmap[createKeySQL(v)] = true; 506 bool[string] newmap; 507 foreach(v; newRelIds) 508 newmap[createKeySQL(v)] = true; 509 string[] keysToDelete; 510 string[] keysToAdd; 511 foreach(v; newmap.keys) 512 if ((v in oldmap) is null) 513 keysToAdd ~= v; 514 foreach(v; oldmap.keys) 515 if ((v in newmap) is null) 516 keysToDelete ~= v; 517 if (keysToAdd.length > 0) { 518 Statement stmt = conn.createStatement(); 519 scope(exit) stmt.close(); 520 stmt.executeUpdate(p.joinTable.getInsertSQL(dialect, createKeySQL(thisId), keysToAdd)); 521 } 522 if (keysToDelete.length > 0) { 523 Statement stmt = conn.createStatement(); 524 scope(exit) stmt.close(); 525 stmt.executeUpdate(p.joinTable.getDeleteSQL(dialect, createKeySQL(thisId), keysToDelete)); 526 } 527 } 528 529 private void deleteRelations(const PropertyInfo p, Object obj) { 530 Variant thisId = p.entity.getKey(obj); 531 Variant[] oldRelIds = readRelationIds(p, thisId); 532 string[] ids; 533 foreach(v; oldRelIds) 534 ids ~= createKeySQL(v); 535 if (ids.length > 0) { 536 Statement stmt = conn.createStatement(); 537 scope(exit) stmt.close(); 538 stmt.executeUpdate(p.joinTable.getDeleteSQL(dialect, createKeySQL(thisId), ids)); 539 } 540 } 541 542 543 /// Persist the given transient instance, first assigning a generated identifier if not assigned; returns generated value 544 override Variant save(Object obj) { 545 auto info = metaData.findEntityForObject(obj); 546 if (!info.isKeySet(obj)) { 547 if (info.getKeyProperty().generated) { 548 // autogenerated on DB side 549 string query = dialect.appendInsertToFetchGeneratedKey(metaData.generateInsertNoKeyForEntity(dialect, info), info); 550 PreparedStatement stmt = conn.prepareStatement(query); 551 scope(exit) stmt.close(); 552 metaData.writeAllColumns(obj, stmt, 1, true); 553 Variant generatedKey; 554 stmt.executeUpdate(generatedKey); 555 info.setKey(obj, generatedKey); 556 } else if (info.getKeyProperty().generatorFunc !is null) { 557 // has generator function 558 Variant generatedKey = info.getKeyProperty().generatorFunc(conn, info.getKeyProperty()); 559 info.setKey(obj, generatedKey); 560 string query = metaData.generateInsertAllFieldsForEntity(dialect, info); 561 PreparedStatement stmt = conn.prepareStatement(query); 562 scope(exit) stmt.close(); 563 metaData.writeAllColumns(obj, stmt, 1); 564 stmt.executeUpdate(); 565 } else { 566 throw new PropertyValueException("Key is not set and no generator is specified"); 567 } 568 } else { 569 string query = metaData.generateInsertAllFieldsForEntity(dialect, info); 570 PreparedStatement stmt = conn.prepareStatement(query); 571 scope(exit) stmt.close(); 572 metaData.writeAllColumns(obj, stmt, 1); 573 stmt.executeUpdate(); 574 } 575 Variant key = info.getKey(obj); 576 putToCache(info.name, key, obj); 577 saveRelations(info, obj); 578 return key; 579 } 580 581 /// Persist the given transient instance. 582 override void persist(Object obj) { 583 auto info = metaData.findEntityForObject(obj); 584 enforceHelper!TransientObjectException(info.isKeySet(obj), "Cannot persist entity w/o key assigned"); 585 string query = metaData.generateInsertAllFieldsForEntity(dialect, info); 586 PreparedStatement stmt = conn.prepareStatement(query); 587 scope(exit) stmt.close(); 588 metaData.writeAllColumns(obj, stmt, 1); 589 stmt.executeUpdate(); 590 Variant key = info.getKey(obj); 591 putToCache(info.name, key, obj); 592 saveRelations(info, obj); 593 } 594 595 override void update(Object obj) { 596 auto info = metaData.findEntityForObject(obj); 597 enforceHelper!TransientObjectException(info.isKeySet(obj), "Cannot persist entity w/o key assigned"); 598 string query = metaData.generateUpdateForEntity(dialect, info); 599 //writeln("Query: " ~ query); 600 { 601 PreparedStatement stmt = conn.prepareStatement(query); 602 scope(exit) stmt.close(); 603 int columnCount = metaData.writeAllColumns(obj, stmt, 1, true); 604 info.keyProperty.writeFunc(obj, stmt, columnCount + 1); 605 stmt.executeUpdate(); 606 } 607 updateRelations(info, obj); 608 } 609 610 // renamed from Session.delete since delete is D keyword 611 override void remove(Object obj) { 612 auto info = metaData.findEntityForObject(obj); 613 deleteRelations(info, obj); 614 string query = "DELETE FROM " ~ dialect.quoteIfNeeded(info.tableName) ~ " WHERE " ~ dialect.quoteIfNeeded(info.getKeyProperty().columnName) ~ "=?"; 615 PreparedStatement stmt = conn.prepareStatement(query); 616 info.getKeyProperty().writeFunc(obj, stmt, 1); 617 stmt.executeUpdate(); 618 } 619 620 /// Create a new instance of Query for the given HQL query string 621 override Query createQuery(string queryString) { 622 return new QueryImpl(this, queryString); 623 } 624 } 625 626 /// Implementation of HibernateD SessionFactory 627 class SessionFactoryImpl : SessionFactory { 628 // Configuration cfg; 629 // Mapping mapping; 630 // Settings settings; 631 // EventListeners listeners; 632 // SessionFactoryObserver observer; 633 private bool closed; 634 private EntityMetaData metaData; 635 Dialect dialect; 636 DataSource connectionPool; 637 638 SessionImpl[] activeSessions; 639 640 641 DBInfo _dbInfo; 642 override public DBInfo getDBMetaData() { 643 if (_dbInfo is null) 644 _dbInfo = new DBInfo(dialect, metaData); 645 return _dbInfo; 646 } 647 648 649 void sessionClosed(SessionImpl session) { 650 foreach(i, item; activeSessions) { 651 if (item == session) { 652 remove(activeSessions, i); 653 } 654 } 655 } 656 657 this(EntityMetaData metaData, Dialect dialect, DataSource connectionPool) { 658 //writeln("Creating session factory"); 659 this.metaData = metaData; 660 this.dialect = dialect; 661 this.connectionPool = connectionPool; 662 } 663 664 // this(Configuration cfg, Mapping mapping, Settings settings, EventListeners listeners, SessionFactoryObserver observer) { 665 // this.cfg = cfg; 666 // this.mapping = mapping; 667 // this.settings = settings; 668 // this.listeners = listeners; 669 // this.observer = observer; 670 // if (observer !is null) 671 // observer.sessionFactoryCreated(this); 672 // } 673 private void checkClosed() { 674 enforceHelper!SessionException(!closed, "Session factory is closed"); 675 } 676 677 override void close() { 678 //writeln("Closing session factory"); 679 checkClosed(); 680 closed = true; 681 // if (observer !is null) 682 // observer.sessionFactoryClosed(this); 683 // TODO: 684 } 685 686 bool isClosed() { 687 return closed; 688 } 689 690 Session openSession() { 691 checkClosed(); 692 SessionImpl session = new SessionImpl(this, metaData, dialect, connectionPool); 693 activeSessions ~= session; 694 return session; 695 } 696 } 697 698 struct ObjectList { 699 Object[] list; 700 void add(Object obj) { 701 foreach(v; list) { 702 if (v == obj) { 703 return; // avoid duplicates 704 } 705 } 706 list ~= obj; 707 } 708 @property int length() { return cast(int)list.length; } 709 ref Object opIndex(size_t index) { 710 return list[index]; 711 } 712 } 713 714 Variant normalize(Variant v) { 715 // variants of different type are not equal, normalize to make sure that keys Variant(1) and Variant(1L) are the same 716 if (v.convertsTo!long) 717 return Variant(v.get!long); 718 else if (v.convertsTo!ulong) 719 return Variant(v.get!ulong); 720 return Variant(v.toString()); 721 } 722 723 /// task to load reference entity 724 class PropertyLoadItem { 725 const PropertyInfo property; 726 private ObjectList[Variant] _map; // ID to object list 727 this(const PropertyInfo property) { 728 this.property = property; 729 } 730 @property ref ObjectList[Variant] map() { return _map; } 731 @property Variant[] keys() { return _map.keys; } 732 @property int length() { return cast(int)_map.length; } 733 ObjectList * opIndex(Variant key) { 734 Variant id = normalize(key); 735 if ((id in _map) is null) { 736 _map[id] = ObjectList(); 737 } 738 //assert(length > 0); 739 return &_map[id]; 740 } 741 void add(ref Variant id, Object obj) { 742 auto item = opIndex(id); 743 item.add(obj); 744 //assert(item.length == opIndex(id).length); 745 } 746 string createCommaSeparatedKeyList() { 747 assert(map.length > 0); 748 return .createCommaSeparatedKeyList(map.keys); 749 } 750 } 751 752 string createKeySQL(Variant id) { 753 if (id.convertsTo!long || id.convertsTo!ulong) { 754 return id.toString(); 755 } else { 756 return "'" ~ id.toString() ~ "'"; 757 } 758 } 759 760 string createKeyPairSQL(Variant id1, Variant id2) { 761 return "(" ~ createKeySQL(id1) ~ ", " ~ createKeySQL(id2) ~ ")"; 762 } 763 764 string createCommaSeparatedKeyList(Variant[] list) { 765 assert(list.length > 0); 766 string res; 767 foreach(v; list) { 768 if (res.length > 0) 769 res ~= ", "; 770 res ~= createKeySQL(v); 771 } 772 return res; 773 } 774 775 /// task to load reference entity 776 class EntityCollections { 777 private ObjectList[Variant] _map; // ID to object list 778 @property ref ObjectList[Variant] map() { return _map; } 779 @property Variant[] keys() { return _map.keys; } 780 @property int length() { return cast(int)_map.length; } 781 ref Object[] opIndex(Variant key) { 782 //writeln("searching for key " ~ key.toString); 783 Variant id = normalize(key); 784 if ((id in _map) is null) { 785 //writeln("creating new item"); 786 _map[id] = ObjectList(); 787 } 788 //assert(length > 0); 789 //writeln("returning item"); 790 return _map[id].list; 791 } 792 void add(ref Variant id, Object obj) { 793 auto item = opIndex(id); 794 //writeln("item count = " ~ to!string(item.length)); 795 item ~= obj; 796 } 797 } 798 799 class PropertyLoadMap { 800 private PropertyLoadItem[const PropertyInfo] _map; 801 PropertyLoadItem opIndex(const PropertyInfo prop) { 802 if ((prop in _map) is null) { 803 //writeln("creating new PropertyLoadItem for " ~ prop.propertyName); 804 _map[prop] = new PropertyLoadItem(prop); 805 } 806 assert(_map.length > 0); 807 return _map[prop]; 808 } 809 PropertyLoadItem remove(const PropertyInfo pi) { 810 PropertyLoadItem item = _map[pi]; 811 _map.remove(pi); 812 return item; 813 } 814 @property ref PropertyLoadItem[const PropertyInfo] map() { return _map; } 815 @property int length() { return cast(int)_map.length; } 816 @property const (PropertyInfo)[] keys() { return _map.keys; } 817 void add(const PropertyInfo property, Variant id, Object obj) { 818 auto item = opIndex(property); 819 item.add(id, obj); 820 //assert(item.length > 0); 821 } 822 } 823 824 /// Implementation of HibernateD Query 825 class QueryImpl : Query 826 { 827 SessionImpl sess; 828 ParsedQuery query; 829 ParameterValues params; 830 this(SessionImpl sess, string queryString) { 831 this.sess = sess; 832 //writeln("QueryImpl(): HQL: " ~ queryString); 833 QueryParser parser = new QueryParser(sess.metaData, queryString); 834 //writeln("parsing"); 835 this.query = parser.makeSQL(sess.dialect); 836 //writeln("SQL: " ~ this.query.sql); 837 params = query.createParams(); 838 //writeln("exiting QueryImpl()"); 839 } 840 841 ///Get the query string. 842 override string getQueryString() { 843 return query.hql; 844 } 845 846 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 847 override Object uniqueObject() { 848 return uniqueResult(cast(Object)null); 849 } 850 851 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer. 852 override Object uniqueObject(Object obj) { 853 Object[] rows = listObjects(obj); 854 if (rows == null) 855 return null; 856 enforceHelper!NonUniqueResultException(rows.length == 1, "Query returned more than one object: " ~ getQueryString()); 857 return rows[0]; 858 } 859 860 /// Convenience method to return a single instance that matches the query, or null if the query returns no results. 861 override Variant[] uniqueRow() { 862 Variant[][] rows = listRows(); 863 if (rows == null) 864 return null; 865 enforceHelper!NonUniqueResultException(rows.length == 1, "Query returned more than one row: " ~ getQueryString()); 866 return rows[0]; 867 } 868 869 private FromClauseItem findRelation(FromClauseItem from, const PropertyInfo prop) { 870 for (int i=0; i<query.from.length; i++) { 871 FromClauseItem f = query.from[i]; 872 if (f.base == from && f.baseProperty == prop) 873 return f; 874 if (f.entity == prop.referencedEntity && from.base == f) 875 return f; 876 } 877 return null; 878 } 879 880 private Object readRelations(Object objectContainer, DataSetReader r, PropertyLoadMap loadMap) { 881 Object[] relations = new Object[query.select.length]; 882 //writeln("select clause len = " ~ to!string(query.select.length)); 883 // read all related objects from DB row 884 for (int i = 0; i < query.select.length; i++) { 885 FromClauseItem from = query.select[i].from; 886 //writeln("reading " ~ from.entityName); 887 Object row; 888 if (!from.entity.isKeyNull(r, from.startColumn)) { 889 //writeln("key is not null"); 890 Variant key = from.entity.getKey(r, from.startColumn); 891 //writeln("key is " ~ key.toString); 892 row = sess.peekFromCache(from.entity.name, key); 893 if (row is null) { 894 //writeln("row not found in cache"); 895 row = (objectContainer !is null && i == 0) ? objectContainer : from.entity.createEntity(); 896 //writeln("reading all columns"); 897 sess.metaData.readAllColumns(row, r, from.startColumn); 898 sess.putToCache(from.entity.name, key, row); 899 } else if (objectContainer !is null) { 900 //writeln("copying all properties to existing container"); 901 from.entity.copyAllProperties(objectContainer, row); 902 } 903 } 904 relations[i] = row; 905 } 906 //writeln("fill relations..."); 907 // fill relations 908 for (int i = 0; i < query.select.length; i++) { 909 if (relations[i] is null) 910 continue; 911 FromClauseItem from = query.select[i].from; 912 auto ei = from.entity; 913 for (int j=0; j<ei.length; j++) { 914 auto pi = ei[j]; 915 if (pi.oneToOne || pi.manyToOne) { 916 static if (TRACE_REFS) writeln("updating relations for " ~ from.pathString ~ "." ~ pi.propertyName); 917 FromClauseItem rfrom = findRelation(from, pi); 918 if (rfrom !is null && rfrom.selectIndex >= 0) { 919 Object rel = relations[rfrom.selectIndex]; 920 pi.setObjectFunc(relations[i], rel); 921 } else { 922 if (pi.columnName !is null) { 923 static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " has column name"); 924 if (r.isNull(from.startColumn + pi.columnOffset)) { 925 // FK is null, set NULL to field 926 pi.setObjectFunc(relations[i], null); 927 static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " has null FK"); 928 } else { 929 Variant id = r.getVariant(from.startColumn + pi.columnOffset); 930 Object existing = sess.peekFromCache(pi.referencedEntity.name, id); 931 if (existing !is null) { 932 static if (TRACE_REFS) writeln("existing relation found in cache"); 933 pi.setObjectFunc(relations[i], existing); 934 } else { 935 // FK is not null 936 if (pi.lazyLoad) { 937 // lazy load 938 static if (TRACE_REFS) writeln("scheduling lazy load for " ~ from.pathString ~ "." ~ pi.propertyName ~ " with FK " ~ id.toString); 939 LazyObjectLoader loader = new LazyObjectLoader(sess.accessor, pi, id); 940 pi.setObjectDelegateFunc(relations[i], &loader.load); 941 } else { 942 // delayed load 943 static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " with FK " ~ id.toString() ~ " will be loaded later"); 944 loadMap.add(pi, id, relations[i]); // to load later 945 } 946 } 947 } 948 } else { 949 // TODO: 950 assert(false, "relation " ~ pi.propertyName ~ " has no column name. To be implemented."); 951 } 952 } 953 } else if (pi.oneToMany || pi.manyToMany) { 954 Variant id = ei.getKey(relations[i]); 955 if (pi.lazyLoad) { 956 // lazy load 957 static if (TRACE_REFS) writeln("creating lazy loader for " ~ from.pathString ~ "." ~ pi.propertyName ~ " by FK " ~ id.toString); 958 LazyCollectionLoader loader = new LazyCollectionLoader(sess.accessor, pi, id); 959 pi.setCollectionDelegateFunc(relations[i], &loader.load); 960 } else { 961 // delayed load 962 static if (TRACE_REFS) writeln("Relation " ~ from.pathString ~ "." ~ pi.propertyName ~ " will be loaded later by FK " ~ id.toString); 963 loadMap.add(pi, id, relations[i]); // to load later 964 } 965 } 966 } 967 } 968 return relations[0]; 969 } 970 971 /// Return the query results as a List of entity objects 972 override Object[] listObjects() { 973 return listObjects(null); 974 } 975 976 private void delayedLoadRelations(PropertyLoadMap loadMap) { 977 auto types = loadMap.keys; 978 static if (TRACE_REFS) writeln("delayedLoadRelations " ~ to!string(loadMap.length)); 979 foreach(pi; types) { 980 static if (TRACE_REFS) writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName); 981 assert(pi.referencedEntity !is null); 982 auto map = loadMap.remove(pi); 983 if (map.length == 0) 984 continue; 985 //writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName); 986 string keys = map.createCommaSeparatedKeyList(); 987 if (pi.oneToOne || pi.manyToOne) { 988 if (pi.columnName !is null) { 989 Variant[] unknownKeys; 990 Object[] list = sess.lookupCache(pi.referencedEntity.name, map.keys, unknownKeys); 991 if (unknownKeys.length > 0) { 992 string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ createCommaSeparatedKeyList(unknownKeys) ~ ")"; 993 static if (TRACE_REFS) writeln("delayedLoadRelations: loading " ~ pi.propertyName ~ " HQL: " ~ hql); 994 QueryImpl q = cast(QueryImpl)sess.createQuery(hql); 995 Object[] fromDB = q.listObjects(null, loadMap); 996 list ~= fromDB; 997 static if (TRACE_REFS) writeln("delayedLoadRelations: objects loaded " ~ to!string(fromDB.length)); 998 } else { 999 static if (TRACE_REFS) writeln("all objects found in cache"); 1000 } 1001 static if (TRACE_REFS) writeln("delayedLoadRelations: updating"); 1002 foreach(rel; list) { 1003 static if (TRACE_REFS) writeln("delayedLoadRelations: reading key from " ~ pi.referencedEntity.name); 1004 Variant key = pi.referencedEntity.getKey(rel); 1005 //writeln("map length before: " ~ to!string(map.length)); 1006 auto objectsToUpdate = map[key].list; 1007 //writeln("map length after: " ~ to!string(map.length)); 1008 //writeln("updating relations with key " ~ key.toString() ~ " (" ~ to!string(objectsToUpdate.length) ~ ")"); 1009 foreach(obj; objectsToUpdate) { 1010 pi.setObjectFunc(obj, rel); 1011 } 1012 } 1013 } else { 1014 assert(false, "Delayed loader for non-join column is not yet implemented for OneToOne and ManyToOne"); 1015 } 1016 } else if (pi.oneToMany || pi.manyToMany) { 1017 string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " ~ pi.referencedPropertyName ~ "." ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ keys ~ ")"; 1018 static if (TRACE_REFS) writeln("delayedLoadRelations: loading " ~ pi.propertyName ~ " HQL: " ~ hql); 1019 QueryImpl q = cast(QueryImpl)sess.createQuery(hql); 1020 assert(q !is null); 1021 Object[] list = q.listObjects(null, loadMap); 1022 static if (TRACE_REFS) writeln("delayedLoadRelations oneToMany/manyToMany: objects loaded " ~ to!string(list.length)); 1023 EntityCollections collections = new EntityCollections(); 1024 // group by referenced PK 1025 foreach(rel; list) { 1026 static if (TRACE_REFS) writeln("delayedLoadRelations oneToMany/manyToMany: reading reference from " ~ pi.referencedEntity.name ~ "." ~ pi.referencedProperty.propertyName ~ " joinColumn=" ~ pi.referencedProperty.columnName); 1027 assert(pi.referencedProperty.manyToOne, "property referenced from OneToMany should be ManyToOne"); 1028 assert(pi.referencedProperty.getObjectFunc !is null); 1029 assert(rel !is null); 1030 //writeln("delayedLoadRelations oneToMany: reading object " ~ rel.classinfo.toString); 1031 Object obj = pi.referencedProperty.getObjectFunc(rel); 1032 //writeln("delayedLoadRelations oneToMany: object is read"); 1033 if (obj !is null) { 1034 //writeln("delayedLoadRelations oneToMany: object is not null"); 1035 //writeln("pi.entity.name=" ~ pi.entity.name ~ ", obj is " ~ obj.classinfo.toString); 1036 //writeln("obj = " ~ obj.toString); 1037 //writeln("pi.entity.keyProperty=" ~ pi.entity.keyProperty.propertyName); 1038 //assert(pi.entity.keyProperty.getFunc !is null); 1039 //Variant k = pi.entity.keyProperty.getFunc(obj); 1040 //writeln("key=" ~ k.toString); 1041 Variant key = pi.entity.getKey(obj); 1042 collections[key] ~= rel; 1043 //collections.add(k, rel); 1044 } 1045 } 1046 // update objects 1047 foreach(key; collections.keys) { 1048 auto objectsToUpdate = map[key].list; 1049 foreach(obj; objectsToUpdate) { 1050 pi.setCollectionFunc(obj, collections[key]); 1051 } 1052 } 1053 } 1054 } 1055 } 1056 1057 /// Return the query results as a List of entity objects 1058 Object[] listObjects(Object placeFirstObjectHere) { 1059 PropertyLoadMap loadMap = new PropertyLoadMap(); 1060 return listObjects(placeFirstObjectHere, loadMap); 1061 } 1062 1063 /// Return the query results as a List of entity objects 1064 Object[] listObjects(Object placeFirstObjectHere, PropertyLoadMap loadMap) { 1065 static if (TRACE_REFS) writeln("Entering listObjects " ~ query.hql); 1066 auto ei = query.entity; 1067 enforceHelper!SessionException(ei !is null, "No entity expected in result of query " ~ getQueryString()); 1068 params.checkAllParametersSet(); 1069 sess.checkClosed(); 1070 1071 Object[] res; 1072 1073 1074 //writeln("SQL: " ~ query.sql); 1075 PreparedStatement stmt = sess.conn.prepareStatement(query.sql); 1076 scope(exit) stmt.close(); 1077 params.applyParams(stmt); 1078 ResultSet rs = stmt.executeQuery(); 1079 assert(query.select !is null && query.select.length > 0); 1080 int startColumn = query.select[0].from.startColumn; 1081 { 1082 scope(exit) rs.close(); 1083 while(rs.next()) { 1084 //writeln("read relations..."); 1085 Object row = readRelations(res.length > 0 ? null : placeFirstObjectHere, rs, loadMap); 1086 if (row !is null) 1087 res ~= row; 1088 } 1089 } 1090 if (loadMap.length > 0) { 1091 static if (TRACE_REFS) writeln("relation properties scheduled for load: loadMap.length == " ~ to!string(loadMap.length)); 1092 delayedLoadRelations(loadMap); 1093 } 1094 static if (TRACE_REFS) writeln("Exiting listObjects " ~ query.hql); 1095 return res.length > 0 ? res : null; 1096 } 1097 1098 /// Return the query results as a List which each row as Variant array 1099 override Variant[][] listRows() { 1100 params.checkAllParametersSet(); 1101 sess.checkClosed(); 1102 1103 Variant[][] res; 1104 1105 //writeln("SQL: " ~ query.sql); 1106 PreparedStatement stmt = sess.conn.prepareStatement(query.sql); 1107 scope(exit) stmt.close(); 1108 params.applyParams(stmt); 1109 ResultSet rs = stmt.executeQuery(); 1110 scope(exit) rs.close(); 1111 while(rs.next()) { 1112 Variant[] row = new Variant[query.colCount]; 1113 for (int i = 1; i<=query.colCount; i++) 1114 row[i - 1] = rs.getVariant(i); 1115 res ~= row; 1116 } 1117 return res.length > 0 ? res : null; 1118 } 1119 1120 /// Bind a value to a named query parameter. 1121 override protected Query setParameterVariant(string name, Variant val) { 1122 params.setParameter(name, val); 1123 return this; 1124 } 1125 } 1126 1127 class LazyObjectLoader { 1128 const PropertyInfo pi; 1129 Variant id; 1130 SessionAccessor sess; 1131 this(SessionAccessor sess, const PropertyInfo pi, Variant id) { 1132 static if (TRACE_REFS) writeln("Created lazy loader for " ~ pi.referencedEntityName ~ " with id " ~ id.toString); 1133 this.pi = pi; 1134 this.id = id; 1135 this.sess = sess; 1136 } 1137 Object load() { 1138 static if (TRACE_REFS) writeln("LazyObjectLoader.load()"); 1139 static if (TRACE_REFS) writeln("lazy loading of " ~ pi.referencedEntityName ~ " with id " ~ id.toString); 1140 return sess.get().loadObject(pi.referencedEntityName, id); 1141 } 1142 } 1143 1144 class LazyCollectionLoader { 1145 const PropertyInfo pi; 1146 Variant fk; 1147 SessionAccessor sess; 1148 this(SessionAccessor sess, const PropertyInfo pi, Variant fk) { 1149 assert(!pi.oneToMany || (pi.referencedEntity !is null && pi.referencedProperty !is null), "LazyCollectionLoader: No referenced property specified for OneToMany foreign key column"); 1150 static if (TRACE_REFS) writeln("Created lazy loader for collection for references " ~ pi.entity.name ~ "." ~ pi.propertyName ~ " by id " ~ fk.toString); 1151 this.pi = pi; 1152 this.fk = fk; 1153 this.sess = sess; 1154 } 1155 Object[] load() { 1156 static if (TRACE_REFS) writeln("LazyObjectLoader.load()"); 1157 static if (TRACE_REFS) writeln("lazy loading of references " ~ pi.entity.name ~ "." ~ pi.propertyName ~ " by id " ~ fk.toString); 1158 Object[] res = sess.get().loadReferencedObjects(pi.entity, pi.propertyName, fk); 1159 return res; 1160 } 1161 } 1162