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