mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-25 11:32:07 +00:00
When a *ToOne association is fetched with only its id property, the foreign key column already exists on the owning table so no join is required. Previously this always generated an unnecessary join. - OrmQueryProperties: add includesExactly() and withAddedInclude() to support adding an include without mutating a shared/cached instance (the included set and its precomputed hash prefix are immutable since #3761/#3763, so an "add" must produce a new instance). - OrmQueryDetail: add convertIdFetches(), a bottom-up pass over fetchPaths that replaces an id-only *ToOne fetch with its FK property folded into the parent's select, removing the now-unneeded fetch path. Skips exported (mappedBy) one-to-one associations, which have no local FK column and always require a join. Tracks paths still depended on by a surviving child fetch so their join isn't incorrectly folded away. - Add TestFetchIdOnly covering PathProperties, select(), and fetch() usages, plus a control case confirming the join is retained when more than the id is fetched. - Update EqlParserTest, TestSubQuery and TestMergeCustomer assertions to reflect the new join-free SQL.
402 lines
14 KiB
Java
402 lines
14 KiB
Java
package org.tests.merge;
|
|
|
|
import io.ebean.*;
|
|
import io.ebean.test.LoggedSql;
|
|
import io.ebean.text.PathProperties;
|
|
import io.ebean.xtest.BaseTestCase;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
import java.util.UUID;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
public class TestMergeCustomer extends BaseTestCase {
|
|
|
|
private Random random = new Random();
|
|
|
|
@Test
|
|
public void customerOnly_defaultOptions_expect_updateOnly() {
|
|
|
|
MCustomer mCustomer = partial("cust1", "(id,name,version)");
|
|
mCustomer.setName("NotCust0");
|
|
|
|
LoggedSql.start();
|
|
|
|
DB.merge(mCustomer);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
assertThat(sql).hasSize(1);
|
|
assertSql(sql.get(0)).contains("update mcustomer set name=?, version=? where id=? and version=?");
|
|
}
|
|
|
|
/**
|
|
* So this is effectively the same as a stateless update.
|
|
*/
|
|
@Test
|
|
public void customerOnly_expect_updateOnly() {
|
|
|
|
|
|
MCustomer mCustomer = partial("cust1", "(id,name,version)");
|
|
mCustomer.setName("NotCust1");
|
|
|
|
MergeOptions options = new MergeOptionsBuilder().build();
|
|
|
|
|
|
LoggedSql.start();
|
|
|
|
DB.merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
assertThat(sql).hasSize(1);
|
|
assertSql(sql.get(0)).contains("update mcustomer set name=?, version=? where id=? and version=?");
|
|
}
|
|
|
|
@Test
|
|
public void customerOnly_setClientGeneratedIds_expect_selectAndUpdate() {
|
|
|
|
|
|
MCustomer mCustomer = partial("cust2", "(id,name,version)");
|
|
mCustomer.setName("NotCust2");
|
|
|
|
MergeOptions options = new MergeOptionsBuilder().setClientGeneratedIds().build();
|
|
|
|
LoggedSql.start();
|
|
|
|
server().merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
assertThat(sql).hasSize(2);
|
|
assertSql(sql.get(0)).contains("select t0.id from mcustomer t0 where t0.id = ?");
|
|
assertSql(sql.get(1)).contains("update mcustomer set name=?, version=? where id=? and version=?");
|
|
}
|
|
|
|
@Test
|
|
public void customerWithAddresses_setClientGeneratedIds_expect_selectAndUpdate() {
|
|
|
|
MCustomer mCustomer = partial("cust3", "(id,name,version,shippingAddress(*),billingAddress(*))");
|
|
mCustomer.setName("NotCust3");
|
|
mCustomer.getBillingAddress().setStreet("modBillStreet");
|
|
mCustomer.getShippingAddress().setCity("modShipCity");
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("shippingAddress")
|
|
.addPath("billingAddress")
|
|
.setClientGeneratedIds()
|
|
.build();
|
|
|
|
LoggedSql.start();
|
|
|
|
server().merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
assertThat(sql).hasSize(6);
|
|
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
|
|
assertSql(sql.get(1)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
|
|
assertSqlBind(sql, 2, 3);
|
|
assertThat(sql.get(5)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
|
|
}
|
|
|
|
@Test
|
|
public void customerWithAddresses_newAddress_setClientGeneratedIds_expect_insertAddress() {
|
|
|
|
|
|
MCustomer mCustomer = partial("cust3", "(id,name,version,shippingAddress(*),billingAddress(*))");
|
|
mCustomer.setName("NotCust3");
|
|
|
|
// new billing address - no Id value so must be insert
|
|
mCustomer.setBillingAddress(new MAddress("Short", "Mid Wicket"));
|
|
mCustomer.getShippingAddress().setCity("modShipCity");
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("shippingAddress")
|
|
.addPath("billingAddress")
|
|
.setClientGeneratedIds()
|
|
.build();
|
|
|
|
LoggedSql.start();
|
|
|
|
server().merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
assertThat(sql).hasSize(8);
|
|
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
|
|
assertSql(sql.get(1)).contains("insert into maddress (id, street, city, version) values (?,?,?,?)");
|
|
assertSqlBind(sql.get(2));
|
|
assertThat(sql.get(4)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
|
|
assertSqlBind(sql.get(5));
|
|
assertThat(sql.get(7)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
|
|
}
|
|
|
|
@Test
|
|
public void customerWithAddresses_newAddressWithId_setClientGeneratedIds_expect_additionalCheckForAddressInsert() {
|
|
|
|
|
|
MCustomer mCustomer = partial("cust3", "(id,name,version,shippingAddress(*),billingAddress(*))");
|
|
mCustomer.setName("NotCust3");
|
|
|
|
// new billing address - has Id value + setClientGeneratedIds ... so extra query to check
|
|
MAddress mAddress = new MAddress("Short", "Mid Wicket");
|
|
mAddress.setId(UUID.randomUUID());
|
|
mCustomer.setBillingAddress(mAddress);
|
|
mCustomer.getShippingAddress().setCity("modShipCity");
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("shippingAddress")
|
|
.addPath("billingAddress")
|
|
.setClientGeneratedIds() // As we are using clientIds ... we don't know if the new UUID is an insert or update without checking
|
|
.build();
|
|
|
|
LoggedSql.start();
|
|
|
|
server().merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
assertThat(sql).hasSize(9);
|
|
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
|
|
|
|
// Additional check to see if the address with the unknown UUID is 'insert' or 'update'
|
|
assertSql(sql.get(1)).contains("select t0.id from maddress t0 where t0.id = ?");
|
|
assertSql(sql.get(2)).contains("insert into maddress (id, street, city, version) values (?,?,?,?)");
|
|
assertSqlBind(sql.get(3));
|
|
assertThat(sql.get(5)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
|
|
assertSqlBind(sql.get(6));
|
|
assertThat(sql.get(8)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
|
|
}
|
|
|
|
@Test
|
|
public void assocOne_onlineIsNull() {
|
|
|
|
MCustomer c = new MCustomer("Null Address");
|
|
c.setBillingAddress(new MAddress("Cow corner", "Mid Wicket"));
|
|
|
|
DB.save(c);
|
|
|
|
MCustomer mCustomer = rebuildViaJson(c);
|
|
|
|
MAddress mAddress = new MAddress("Silly", "Mid Wicket");
|
|
mAddress.setId(UUID.randomUUID());
|
|
mCustomer.setShippingAddress(mAddress);
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("shippingAddress")
|
|
.addPath("billingAddress")
|
|
.setClientGeneratedIds()
|
|
.build();
|
|
|
|
DB.merge(mCustomer, options);
|
|
|
|
}
|
|
|
|
private MCustomer rebuildViaJson(MCustomer input) {
|
|
String asJson = DB.json().toJson(input);
|
|
return DB.json().toBean(MCustomer.class, asJson);
|
|
}
|
|
|
|
@Test
|
|
public void whenContacts_isNull_expect_deleteContacts() {
|
|
|
|
MCustomer mCustomer = partial("cust6", "(id,name,version,shippingAddress(id),billingAddress(id),contacts(*))");
|
|
|
|
// null contacts ... but in path so this means - delete all contacts
|
|
mCustomer.setContacts(null);
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("contacts")
|
|
.build();
|
|
|
|
LoggedSql.start();
|
|
server().merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
if (isPersistBatchOnCascade()) {
|
|
assertThat(sql).hasSize(26);
|
|
}
|
|
assertSql(sql.get(0)).contains("select t0.id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
|
|
assertSql(sql.get(1)).contains("delete from mcontact_message where contact_id = ?");
|
|
assertThat(sql.get(4)).contains("delete from mcontact where id=?");
|
|
assertThat(sql.get(25)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
|
|
}
|
|
|
|
|
|
@Test
|
|
public void whenContacts_isEmpty_expect_deleteContacts() {
|
|
|
|
MCustomer mCustomer = partial("cust6", "(id,name,version,shippingAddress(id),billingAddress(id),contacts(*))");
|
|
|
|
// empty contacts ... but in path so this means - delete all contacts
|
|
mCustomer.setContacts(new ArrayList<>());
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("contacts")
|
|
.build();
|
|
|
|
LoggedSql.start();
|
|
server().merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
if (isPersistBatchOnCascade()) {
|
|
assertThat(sql).hasSize(26);
|
|
}
|
|
assertSql(sql.get(0)).contains("select t0.id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
|
|
assertSql(sql.get(1)).contains("delete from mcontact_message where contact_id = ?");
|
|
assertThat(sql.get(4)).contains("delete from mcontact where id=?");
|
|
assertThat(sql.get(25)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
|
|
}
|
|
|
|
@Test
|
|
public void whenContacts_mixed_expect_deleteInsertUpdateContacts() {
|
|
|
|
MCustomer mCustomer = partial("cust6", "(id,name,version,shippingAddress(id),billingAddress(id),contacts(*))");
|
|
List<MContact> contacts = mCustomer.getContacts();
|
|
|
|
MContact mContact = new MContact("z@a.com", "z", "zed");
|
|
mContact.setId(UUID.randomUUID());
|
|
|
|
contacts.add(mContact);
|
|
|
|
contacts.get(0).setEmail("a@beta.com");
|
|
contacts.get(1).setEmail("b@beta.com");
|
|
contacts.remove(4);
|
|
contacts.remove(2);
|
|
|
|
MContact mContactEnd = new MContact("z@z.com", "zx", "zedXtra");
|
|
mContactEnd.setId(UUID.randomUUID());
|
|
contacts.add(mContactEnd);
|
|
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("contacts")
|
|
.build();
|
|
|
|
LoggedSql.start();
|
|
server().merge(mCustomer, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
if (isPersistBatchOnCascade()) {
|
|
assertThat(sql).hasSize(20);
|
|
assertSql(sql.get(0)).contains("select t0.id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
|
|
assertSql(sql.get(1)).contains("delete from mcontact_message where contact_id = ?");
|
|
assertThat(sql.get(4)).contains("delete from mcontact where id=?");
|
|
assertThat(sql.get(9)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
|
|
}
|
|
|
|
if (isPersistBatchOnCascade()) {
|
|
assertThat(sql.get(10)).contains("insert into mcontact");
|
|
assertThat(sql.get(11)).contains("-- bind(");
|
|
assertThat(sql.get(14)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?");
|
|
} else {
|
|
assertThat(sql.get(6)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?");
|
|
assertThat(sql.get(7)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?");
|
|
assertThat(sql.get(10)).contains("insert into mcontact");
|
|
assertThat(sql.get(11)).contains("insert into mcontact");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void fullMonty() {
|
|
|
|
MCustomer cust1 = customer("monty1");
|
|
|
|
modify(cust1);
|
|
|
|
MergeOptions options = new MergeOptionsBuilder()
|
|
.addPath("billingAddress")
|
|
.addPath("shippingAddress")
|
|
.addPath("contacts")
|
|
.addPath("contacts.messages")
|
|
.setClientGeneratedIds()
|
|
.setDeletePermanent()
|
|
.build();
|
|
|
|
LoggedSql.start();
|
|
server().merge(cust1, options);
|
|
|
|
List<String> sql = LoggedSql.stop();
|
|
if (isPersistBatchOnCascade()) {
|
|
|
|
assertSql(sql.get(0)).contains("select t0.id, t0.shipping_address_id, t0.billing_address_id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
|
|
if (isH2() || isHana()) {
|
|
// with nested OneToMany .. we need a second query to read the contact message ids
|
|
assertSql(sql.get(1)).contains("select t0.contact_id, t0.id from mcontact_message t0 where (t0.contact_id) in (?,?,?,?,?,?,?,?,?,?)");
|
|
}
|
|
assertSql(sql.get(2)).contains("delete from mcontact_message where contact_id = ?");
|
|
assertThat(sql.get(5)).contains("delete from mcontact where id=?");
|
|
assertThat(sql.get(6)).contains("delete from mcontact_message where contact_id = ?");
|
|
assertThat(sql.get(9)).contains("delete from mcontact where id=?");
|
|
|
|
assertThat(sql.get(10)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
|
|
assertThat(sql.get(17)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?");
|
|
assertSqlBind(sql, 18, 21);
|
|
assertThat(sql.get(23)).contains("update mcontact_message set title=?, subject=?, notes=?, version=?, contact_id=? where id=? and version=?");
|
|
}
|
|
}
|
|
|
|
private void modify(MCustomer cust) {
|
|
|
|
List<MContact> contacts = cust.getContacts();
|
|
MContact mContact = new MContact("z@a.com", "z", "zed");
|
|
mContact.setId(UUID.randomUUID());
|
|
|
|
contacts.add(mContact);
|
|
|
|
contacts.get(0).setEmail("a@beta.com");
|
|
contacts.get(1).setEmail("b@beta.com");
|
|
contacts.remove(4);
|
|
contacts.remove(2);
|
|
|
|
cust.setName(cust.getName()+" modified");
|
|
cust.getBillingAddress().setStreet("Short");
|
|
|
|
MAddress mAddress = new MAddress("Broken", "Dreams");
|
|
mAddress.setId(UUID.randomUUID());
|
|
cust.setShippingAddress(null);
|
|
}
|
|
|
|
private MCustomer partial(String name, String fetchGraph) {
|
|
MCustomer cust1 = buildCustomer(name);
|
|
DB.save(cust1);
|
|
|
|
FetchPath fetchPath = PathProperties.parse(fetchGraph);
|
|
String asJson = DB.json().toJson(cust1, fetchPath);
|
|
return DB.json().toBean(MCustomer.class, asJson);
|
|
}
|
|
|
|
private MCustomer customer(String name) {
|
|
|
|
MCustomer cust1 = buildCustomer(name);
|
|
DB.save(cust1);
|
|
|
|
String asJson = DB.json().toJson(cust1);
|
|
|
|
return DB.json().toBean(MCustomer.class, asJson);
|
|
}
|
|
|
|
private MCustomer buildCustomer(String name) {
|
|
|
|
MCustomer c = new MCustomer(name);
|
|
c.setShippingAddress(new MAddress("Fleet st", "London"));
|
|
c.setBillingAddress(new MAddress("Cow corner", "Mid Wicket"));
|
|
|
|
c.getContacts().add(addContact("a@a.com", "a", "alligator"));
|
|
c.getContacts().add(addContact("b@a.com", "b", "beaver"));
|
|
c.getContacts().add(addContact("c@a.com", "c", "crow"));
|
|
c.getContacts().add(addContact("d@a.com", "d", "dog"));
|
|
c.getContacts().add(addContact("e@a.com", "e", "ent"));
|
|
c.getContacts().add(addContact("f@a.com", "f", "frog"));
|
|
|
|
return c;
|
|
}
|
|
|
|
private MContact addContact(String email, String first, String last) {
|
|
MContact mContact = new MContact(email, first, last);
|
|
int i = 1 + random.nextInt(2);
|
|
for (int j = 0; j < i; j++) {
|
|
mContact.getMessages().add(new MContactMessage(first+" "+i, last+" "+i));
|
|
}
|
|
return mContact;
|
|
}
|
|
}
|