Skip to content

Commit 9ce43e6

Browse files
vnayarSingingBush
authored andcommitted
Bug: Using session.list() with @EmbeddedId causes bad cache hits
Currently, when a session is populating its cache, it does so by extracting they "key" of an entity, and comparing it to the "key" as read from database columns. However, this logic only reads a single column, thus, `@EmbeddedId` keys will falsely match in the cache, resulting in a call to `session.list()` returning the same cached Entity multiple times instead of reading each row. While this can be fixed by modifying the `getKey` method of `metadata.EntityInfo`, this is not a complete solution, because the cache first calls `normalize` on the `Variant` which is used as a key. `normalize` is not aware of objects, it has special cases for `long` and `int`, and everything else uses `toString`, meaning that all `Object` instances (unless they implement `toString`) will have the same cache key. This is resolved by modifying `normalize` to simply return a `Variant` containing an object as-is, which relies on `opEquals` being implemented. This means that, if an `@EmbeddedId` property refers to an `@Embeddable` entity that does NOT implement `opEquals`, then it will never be considered a cache-hit. This limitation was added to the documentation and README, so that users wishing for the performance benefit of the cache remember to implement `opEquals`. - session.d: `Variant normalize(Variant v)` is used to form session cache keys, it was modified to support object keys used by `@EmbeddedId`. - metadata.d: `EntityInfo.getKey(DataSetReader r, int startColumn)` has been renamed to `getKeyFromColumns` to disambiguate it from another overload. Added logic to read multiple columns for an `@EmbeddedId` key. - annotations.d: This is the only file using CRLF line endings, standardize it.
1 parent ad4a13b commit 9ce43e6

File tree

6 files changed

+353
-289
lines changed

6 files changed

+353
-289
lines changed

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ To represent this using HibernateD, the following code would be used:
194194
class InvoiceId {
195195
string vendorNo;
196196
string invoiceNo;
197+
198+
// Be sure to implement this to benefit from session caching.
199+
bool opEquals(const InvoiceId o) const @safe {
200+
return vendorNo == o.vendorNo && invoiceNo == o.invoiceNo;
201+
}
197202
}
198203
199204
@Table("invoices")
@@ -203,10 +208,12 @@ class Invoice {
203208
}
204209
```
205210

206-
**Note**: At the time of writing, there are two important limitations.
211+
**Note**: At the time of writing, there are important limitations.
207212
1. The function `DBInfo.updateDbSchema(Connection conn, bool dropTables, bool createTables)`
208213
does not generate schemas with compound keys.
209214
2. The Hibernate annotation `@JoinColumns` (plural) has not yet been implemented, thus,
210215
the `@ManyToOne` and `@ManyToMany` relations are not usable for classes using an
211216
`@EmbeddedId`.
217+
3. The `@Embedded` class referenced via an `@EmbeddedId` property should implement `opEquals`
218+
in order to gain performance benefits from session caching.
212219
These features will be added in future updates.

hdtest/source/embeddedidtest.d

+38-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
module embeddedidtest;
22

3+
4+
import std.algorithm : any;
5+
36
import hibernated.core;
47

58
import testrunner : Test;
@@ -12,6 +15,16 @@ class InvoiceId {
1215
string vendorNo;
1316
// Vendors independently pick an invoiceNo, which may overlap with other vendors.
1417
string invoiceNo;
18+
19+
// Allows session caching to function correctly.
20+
bool opEquals(const InvoiceId o) const @safe {
21+
return vendorNo == o.vendorNo && invoiceNo == o.invoiceNo;
22+
}
23+
24+
// Useful for debugging.
25+
override string toString() const @safe {
26+
return vendorNo ~ ":" ~ invoiceNo;
27+
}
1528
}
1629

1730
@Entity
@@ -40,13 +53,21 @@ class EmbeddedIdTest : HibernateTest {
4053
invoice.invoiceId.invoiceNo = "L1005-2328";
4154
invoice.currency = "EUR";
4255
invoice.amountE4 = 120_3400;
43-
4456
InvoiceId c1Id = sess.save(invoice).get!InvoiceId;
4557
assert(c1Id.vendorNo == "ABC123" && c1Id.invoiceNo == "L1005-2328");
58+
59+
Invoice invoice2 = new Invoice();
60+
invoice2.invoiceId = new InvoiceId();
61+
invoice2.invoiceId.vendorNo = "ABC123";
62+
invoice2.invoiceId.invoiceNo = "L1005-2329";
63+
invoice2.currency = "EUR";
64+
invoice2.amountE4 = 80_1200;
65+
InvoiceId c2Id = sess.save(invoice2).get!InvoiceId;
66+
assert(c2Id.vendorNo == "ABC123" && c2Id.invoiceNo == "L1005-2329");
4667
}
4768

48-
@Test("embeddedid.read.query")
49-
void readQueryTest() {
69+
@Test("embeddedid.read.query.uniqueResult")
70+
void readQueryUniqueTest() {
5071
Session sess = sessionFactory.openSession();
5172
scope(exit) sess.close();
5273

@@ -61,6 +82,20 @@ class EmbeddedIdTest : HibernateTest {
6182
assert(i1.amountE4 == 120_3400);
6283
}
6384

85+
@Test("embeddedid.read.query.list")
86+
void readQueryListTest() {
87+
Session sess = sessionFactory.openSession();
88+
scope(exit) sess.close();
89+
90+
// A query that only partially covers the primary key.
91+
auto r2 = sess.createQuery("FROM Invoice WHERE invoiceId.vendorNo = :VendorNo")
92+
.setParameter("VendorNo", "ABC123");
93+
Invoice[] i2List = r2.list!Invoice();
94+
assert(i2List.length == 2);
95+
assert(i2List.any!((Invoice inv) => inv.invoiceId.vendorNo == "ABC123" && inv.invoiceId.invoiceNo == "L1005-2328"));
96+
assert(i2List.any!(inv => inv.invoiceId.vendorNo == "ABC123" && inv.invoiceId.invoiceNo == "L1005-2329"));
97+
}
98+
6499
@Test("embeddedid.read.get")
65100
void readGetTest() {
66101
Session sess = sessionFactory.openSession();

hdtest/source/hibernatetest.d

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import testrunner : Test, BeforeClass, AfterClass;
1111
*/
1212
struct ConnectionParams {
1313
string host;
14-
ushort port;
14+
ushort port;
1515
string database;
16-
string user;
17-
string pass;
16+
string user;
17+
string pass;
1818
}
1919

2020
/**

0 commit comments

Comments
 (0)