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