Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for @EmbeddedId, permitting composite keys. #88

Merged
merged 5 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,45 @@ int main() {
return 0;
}
```

## Additional Features

### Composite Keys

If a database contains tables with a composite primary key, the `@EmbeddedId` can be used to
represent this. The columns that represent the primary key should be in an `@Embeddable` class
which is then referenced in a property annotated with `@EmbeddedId`.

For example, consider a database table created via the following SQL command:

```sql
CREATE TABLE invoices (
vendor_no VARCHAR(8) NOT NULL,
invoice_no VARCHAR(20) NOT NULL,
amount_e4 INTEGER);
ALTER TABLE invoices
ADD CONSTRAINT invoices_pkey PRIMARY KEY (vendor_no, invoice_no);
```

To represent this using HibernateD, the following code would be used:
```
@Embeddable
class InvoiceId {
string vendorNo;
string invoiceNo;
}

@Table("invoices")
class Invoice {
@EmbeddedId InvoiceId invoiceId;
int amountE4;
}
```

**Note**: At the time of writing, there are two important limitations.
1. The function `DBInfo.updateDbSchema(Connection conn, bool dropTables, bool createTables)`
does not generate schemas with compound keys.
2. The Hibernate annotation `@JoinColumns` (plural) has not yet been implemented, thus,
the `@ManyToOne` and `@ManyToMany` relations are not usable for classes using an
`@EmbeddedId`.
These features will be added in future updates.
127 changes: 127 additions & 0 deletions hdtest/source/embeddedidtest.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
module embeddedidtest;

import hibernated.core;

import testrunner : Test;
import hibernatetest : HibernateTest;

// An object representing a composite key, a key with multiple columns to uniquely identify a row.
@Embeddable
class InvoiceId {
// Each vendor has a unique ID.
string vendorNo;
// Vendors independently pick an invoiceNo, which may overlap with other vendors.
string invoiceNo;
}

@Entity
class Invoice {
@EmbeddedId
InvoiceId invoiceId;

string currency;
int amountE4;
}

class EmbeddedIdTest : HibernateTest {
override
EntityMetaData buildSchema() {
return new SchemaInfoImpl!(Invoice, InvoiceId);
}

@Test("embeddedid.creation")
void creationTest() {
Session sess = sessionFactory.openSession();
scope(exit) sess.close();

Invoice invoice = new Invoice();
invoice.invoiceId = new InvoiceId();
invoice.invoiceId.vendorNo = "ABC123";
invoice.invoiceId.invoiceNo = "L1005-2328";
invoice.currency = "EUR";
invoice.amountE4 = 120_3400;

InvoiceId c1Id = sess.save(invoice).get!InvoiceId;
assert(c1Id.vendorNo == "ABC123" && c1Id.invoiceNo == "L1005-2328");
}

@Test("embeddedid.read.query")
void readQueryTest() {
Session sess = sessionFactory.openSession();
scope(exit) sess.close();

auto r1 = sess.createQuery("FROM Invoice WHERE invoiceId.vendorNo = :VendorNo AND invoiceId.invoiceNo = :InvoiceNo")
.setParameter("VendorNo", "ABC123")
.setParameter("InvoiceNo", "L1005-2328");
Invoice i1 = r1.uniqueResult!Invoice();
assert(i1 !is null);
assert(i1.invoiceId.vendorNo == "ABC123");
assert(i1.invoiceId.invoiceNo == "L1005-2328");
assert(i1.currency == "EUR");
assert(i1.amountE4 == 120_3400);
}

@Test("embeddedid.read.get")
void readGetTest() {
Session sess = sessionFactory.openSession();
scope(exit) sess.close();

InvoiceId id1 = new InvoiceId();
id1.vendorNo = "ABC123";
id1.invoiceNo = "L1005-2328";
Invoice i1 = sess.get!Invoice(id1);
assert(i1 !is null);
assert(i1.invoiceId.vendorNo == "ABC123");
assert(i1.invoiceId.invoiceNo == "L1005-2328");
assert(i1.currency == "EUR");
assert(i1.amountE4 == 120_3400);
}

@Test("embeddedid.update")
void updateTest() {
Session sess = sessionFactory.openSession();

// Get a record that we will be updating.
InvoiceId id1 = new InvoiceId();
id1.vendorNo = "ABC123";
id1.invoiceNo = "L1005-2328";
Invoice i1 = sess.get!Invoice(id1);
assert(i1 !is null);

i1.currency = "USD";
sess.update(i1);

// Create a new session to prevent caching.
sess.close();
sess = sessionFactory.openSession();

Invoice i2 = sess.get!Invoice(id1);
assert(i2 !is null);
assert(i2.currency == "USD");

sess.close();
}

@Test("embeddedid.delete")
void deleteTest() {
Session sess = sessionFactory.openSession();

// Get an entity to delete.
InvoiceId id1 = new InvoiceId();
id1.vendorNo = "ABC123";
id1.invoiceNo = "L1005-2328";
Invoice i1 = sess.get!Invoice(id1);
assert(i1 !is null);

sess.remove(i1);

// Create a new session to prevent caching.
sess.close();
sess = sessionFactory.openSession();

i1 = sess.get!Invoice(id1);
assert(i1 is null);

sess.close();
}
}
125 changes: 60 additions & 65 deletions hdtest/source/hibernatetest.d
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import testrunner : Test, BeforeClass, AfterClass;
* Generic parameters to connect to a database, independent of the driver.
*/
struct ConnectionParams {
string host;
string host;
ushort port;
string database;
string database;
string user;
string pass;
}
Expand All @@ -23,78 +23,73 @@ struct ConnectionParams {
* `@Test` can simply use a session factory to test hibernate queries.
*/
abstract class HibernateTest {
ConnectionParams connectionParams;
SessionFactory sessionFactory;
ConnectionParams connectionParams;
SessionFactory sessionFactory;

EntityMetaData buildSchema();
EntityMetaData buildSchema();

Dialect buildDialect() {
version (USE_SQLITE) {
Dialect dialect = new SQLiteDialect();
} else version (USE_MYSQL) {
Dialect dialect = new MySQLDialect();
} else version (USE_PGSQL) {
Dialect dialect = new PGSQLDialect();
Dialect buildDialect() {
version (USE_SQLITE) {
Dialect dialect = new SQLiteDialect();
} else version (USE_MYSQL) {
Dialect dialect = new MySQLDialect();
} else version (USE_PGSQL) {
Dialect dialect = new PGSQLDialect();
}
return dialect;
}
return dialect;
}
DataSource buildDataSource(ConnectionParams connectionParams) {
// setup DB connection
version( USE_SQLITE )
{
import ddbc.drivers.sqliteddbc;
string[string] params;
DataSource ds = new ConnectionPoolDataSourceImpl(new SQLITEDriver(), "zzz.db", params);
}
else version( USE_MYSQL )
{
import ddbc.drivers.mysqlddbc;
immutable string url = MySQLDriver.generateUrl(
connectionParams.host, connectionParams.port, connectionParams.database);
string[string] params = MySQLDriver.setUserAndPassword(
connectionParams.user, connectionParams.pass);
DataSource ds = new ConnectionPoolDataSourceImpl(new MySQLDriver(), url, params);
}
else version( USE_PGSQL )
{
import ddbc.drivers.pgsqlddbc;
immutable string url = PGSQLDriver.generateUrl(
connectionParams.host, connectionParams.port, connectionParams.database); // PGSQLDriver.generateUrl( "/tmp", 5432, "testdb" );
string[string] params;
params["user"] = connectionParams.user;
params["password"] = connectionParams.pass;
params["ssl"] = "true";
DataSource buildDataSource(ConnectionParams connectionParams) {
// setup DB connection
version( USE_SQLITE ) {
import ddbc.drivers.sqliteddbc;
string[string] params;
DataSource ds = new ConnectionPoolDataSourceImpl(new SQLITEDriver(), "zzz.db", params);
} else version( USE_MYSQL ) {
import ddbc.drivers.mysqlddbc;
immutable string url = MySQLDriver.generateUrl(
connectionParams.host, connectionParams.port, connectionParams.database);
string[string] params = MySQLDriver.setUserAndPassword(
connectionParams.user, connectionParams.pass);
DataSource ds = new ConnectionPoolDataSourceImpl(new MySQLDriver(), url, params);
} else version( USE_PGSQL ) {
import ddbc.drivers.pgsqlddbc;
immutable string url = PGSQLDriver.generateUrl(
connectionParams.host, connectionParams.port, connectionParams.database); // PGSQLDriver.generateUrl( "/tmp", 5432, "testdb" );
string[string] params;
params["user"] = connectionParams.user;
params["password"] = connectionParams.pass;
params["ssl"] = "true";

DataSource ds = new ConnectionPoolDataSourceImpl(new PGSQLDriver(), url, params);
DataSource ds = new ConnectionPoolDataSourceImpl(new PGSQLDriver(), url, params);
}
return ds;
}
return ds;
}

SessionFactory buildSessionFactory() {
DataSource ds = buildDataSource(connectionParams);
SessionFactory factory = new SessionFactoryImpl(buildSchema(), buildDialect(), ds);
SessionFactory buildSessionFactory() {
DataSource ds = buildDataSource(connectionParams);
SessionFactory factory = new SessionFactoryImpl(buildSchema(), buildDialect(), ds);

writeln("Creating DB Schema");
DBInfo db = factory.getDBMetaData();
{
Connection conn = ds.getConnection();
scope(exit) conn.close();
db.updateDBSchema(conn, true, true);
writeln("Creating DB Schema");
DBInfo db = factory.getDBMetaData();
{
Connection conn = ds.getConnection();
scope(exit) conn.close();
db.updateDBSchema(conn, true, true);
}
return factory;
}
return factory;
}

void setConnectionParams(ConnectionParams connectionParams) {
this.connectionParams = connectionParams;
}
void setConnectionParams(ConnectionParams connectionParams) {
this.connectionParams = connectionParams;
}

@BeforeClass
void setup() {
this.sessionFactory = buildSessionFactory();
}
@BeforeClass
void setup() {
this.sessionFactory = buildSessionFactory();
}

@AfterClass
void teardown() {
this.sessionFactory.close();
}
@AfterClass
void teardown() {
this.sessionFactory.close();
}
}
7 changes: 6 additions & 1 deletion hdtest/source/htestmain.d
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import testrunner : runTests;
import hibernatetest : ConnectionParams;
import generaltest : GeneralTest;
import embeddedtest : EmbeddedTest;
import embeddedidtest : EmbeddedIdTest;
import transactiontest : TransactionTest;

int main(string[] args) {
Expand All @@ -33,10 +34,14 @@ int main(string[] args) {
test2.setConnectionParams(par);
runTests(test2);

TransactionTest test3 = new TransactionTest();
EmbeddedIdTest test3 = new EmbeddedIdTest();
test3.setConnectionParams(par);
runTests(test3);

TransactionTest test4 = new TransactionTest();
test4.setConnectionParams(par);
runTests(test4);

writeln("All scenarios worked successfully");
return 0;
}
Loading
Loading