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