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