From 0c03a8b925ec577251d72555271697ada29ff297 Mon Sep 17 00:00:00 2001 From: rob bygrave Date: Tue, 5 Feb 2019 17:35:58 +1300 Subject: [PATCH] #1630 - ENH: Improve jdbc batch use when persisting ManyToMany intersection changes --- .../server/core/PersistRequestBean.java | 30 +++++- .../server/core/PersistRequestUpdateSql.java | 20 ++++ .../ebeaninternal/server/core/Persister.java | 5 + .../server/deploy/IntersectionRow.java | 8 +- .../server/persist/BatchControl.java | 62 +++++++++++- .../server/persist/DefaultPersister.java | 31 ++++-- .../server/persist/SaveManyBeans.java | 41 ++++---- .../o2m/jointable/TestOneToManyJoinTable.java | 7 +- .../TestOneToManyJoinTableInheritance.java | 15 +-- .../org/tests/sp/TestManyToManySaveTwice.java | 96 +++++++++++++++---- src/test/java/org/tests/sp/model/car/Car.java | 10 ++ .../java/org/tests/sp/model/car/Wheel.java | 9 ++ 12 files changed, 276 insertions(+), 58 deletions(-) diff --git a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java index 8bf7ffd81..0ff175eb4 100644 --- a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java @@ -27,6 +27,7 @@ import io.ebeaninternal.server.persist.BatchedSqlException; import io.ebeaninternal.server.persist.DeleteMode; import io.ebeaninternal.server.persist.Flags; import io.ebeaninternal.server.persist.PersistExecute; +import io.ebeaninternal.server.persist.SaveManyBeans; import io.ebeaninternal.server.transaction.BeanPersistIdMap; import io.ebeanservice.docstore.api.DocStoreUpdate; import io.ebeanservice.docstore.api.DocStoreUpdateContext; @@ -174,7 +175,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP private int pendingPostUpdateNotify; /** - * Set to true when post execute has occured (so includes batch flush). + * Set to true when post execute has occurred (so includes batch flush). */ private boolean postExecute; @@ -183,6 +184,11 @@ public final class PersistRequestBean extends PersistRequest implements BeanP */ private boolean complete; + /** + * Many to many intersection table changes that are held for later batch processing. + */ + private SaveManyBeans saveManyIntersection; + public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager mgr, SpiTransaction t, PersistExecute persistExecute, PersistRequest.Type type, int flags) { @@ -955,6 +961,13 @@ public final class PersistRequestBean extends PersistRequest implements BeanP if (isLogSummary()) { logSummary(); } + saveQueuedManyIntersection(); + } + + private void saveQueuedManyIntersection() { + if (saveManyIntersection != null) { + saveManyIntersection.saveIntersectionBatch(); + } } /** @@ -1119,6 +1132,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP updatedManysOnly = true; setNotifyCache(); addPostCommitListeners(); + saveQueuedManyIntersection(); } notifyCacheOnComplete(); postUpdateNotify(); @@ -1454,4 +1468,18 @@ public final class PersistRequestBean extends PersistRequest implements BeanP public String getSelectLastInsertedId() { return beanDescriptor.getSelectLastInsertedId(publish); } + + /** + * Return true if the intersection table updates should be queued and batched. + */ + public boolean isQueueManyIntersection() { + return !postExecute; + } + + /** + * The intersection table updates to the batch executed later on postExecute. + */ + public void setManyIntersection(SaveManyBeans saveManyIntersection) { + this.saveManyIntersection = saveManyIntersection; + } } diff --git a/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java b/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java index 289592cbe..a1d87937b 100644 --- a/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java +++ b/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java @@ -3,6 +3,7 @@ package io.ebeaninternal.server.core; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiSqlUpdate; import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.server.persist.BatchControl; import io.ebeaninternal.server.persist.PersistExecute; /** @@ -58,6 +59,25 @@ public final class PersistRequestUpdateSql extends PersistRequest { return executeOrQueue(); } + /** + * Execute using jdbc batch. + */ + public void executeAddBatch() { + this.addBatch = true; + persistExecute.executeSqlUpdate(this); + } + + /** + * Add this request to BatchControl to flush later. + */ + public void addToFlushQueue(boolean early) { + BatchControl control = transaction.getBatchControl(); + if (control == null) { + control = persistExecute.createBatchControl(transaction); + } + control.addToFlushQueue(this, early); + } + @Override public int executeNow() { return persistExecute.executeSqlUpdate(this); diff --git a/src/main/java/io/ebeaninternal/server/core/Persister.java b/src/main/java/io/ebeaninternal/server/core/Persister.java index a1b9eedbe..a0bdf1bb5 100644 --- a/src/main/java/io/ebeaninternal/server/core/Persister.java +++ b/src/main/java/io/ebeaninternal/server/core/Persister.java @@ -103,6 +103,11 @@ public interface Persister { */ void visitMetrics(MetricVisitor visitor); + /** + * Execute or queue the update. + */ + void executeOrQueue(SpiSqlUpdate update, SpiTransaction t, boolean queue); + /** * Add the statement to JDBC batch for later execution via executeBatch. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java b/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java index 305469af2..b26f3d81d 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java +++ b/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java @@ -1,8 +1,8 @@ package io.ebeaninternal.server.deploy; -import io.ebean.SqlUpdate; import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.api.SpiSqlUpdate; import io.ebeaninternal.server.core.DefaultSqlUpdate; import io.ebeaninternal.server.expression.DefaultExpressionRequest; import io.ebeaninternal.server.expression.IdInExpression; @@ -45,7 +45,7 @@ public class IntersectionRow { values.put(key, value); } - public SqlUpdate createInsert(SpiEbeanServer server) { + public SpiSqlUpdate createInsert(SpiEbeanServer server) { BindParams bindParams = new BindParams(); @@ -73,7 +73,7 @@ public class IntersectionRow { return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { + public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { BindParams bindParams = new BindParams(); @@ -108,7 +108,7 @@ public class IntersectionRow { return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SqlUpdate createDeleteChildren(SpiEbeanServer server) { + public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server) { BindParams bindParams = new BindParams(); diff --git a/src/main/java/io/ebeaninternal/server/persist/BatchControl.java b/src/main/java/io/ebeaninternal/server/persist/BatchControl.java index 1658d664e..4a3cafa8b 100644 --- a/src/main/java/io/ebeaninternal/server/persist/BatchControl.java +++ b/src/main/java/io/ebeaninternal/server/persist/BatchControl.java @@ -3,6 +3,7 @@ package io.ebeaninternal.server.persist; import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.server.core.PersistRequest; import io.ebeaninternal.server.core.PersistRequestBean; +import io.ebeaninternal.server.core.PersistRequestUpdateSql; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; @@ -10,6 +11,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; /** * Controls the batch ordering of persist requests. @@ -68,6 +70,9 @@ public final class BatchControl { */ private int bufferMax; + private Queue earlyQueue; + private Queue lateQueue; + /** * Create for a given transaction, PersistExecute, default size and getGeneratedKeys. */ @@ -224,14 +229,14 @@ public final class BatchControl { * Flush without resetting the topOrder (maintains the depth info). */ public void flush() throws BatchedSqlException { - flush(false); + flushBuffer(false); } /** * Flush with a reset the topOrder (fully empty the batch). */ public void flushReset() throws BatchedSqlException { - flush(true); + flushBuffer(true); } /** @@ -243,10 +248,22 @@ public final class BatchControl { maxDepth = 0; } + private void flushBuffer(boolean resetTop) throws BatchedSqlException { + flushInternal(resetTop); + flushQueue(earlyQueue); + flushQueue(lateQueue); + } + + private void flushQueue(Queue queue) throws BatchedSqlException { + if (queue != null && queue.flush() && !pstmtHolder.isEmpty()) { + flushPstmtHolder(); + } + } + /** * execute all the requests currently queued or batched. */ - private void flush(boolean resetTop) throws BatchedSqlException { + private void flushInternal(boolean resetTop) throws BatchedSqlException { try { bufferMax = 0; @@ -367,4 +384,43 @@ public final class BatchControl { public int[] execute(String key, boolean getGeneratedKeys) throws SQLException { return pstmtHolder.execute(key, getGeneratedKeys); } + + /** + * Add a SqlUpdate request to execute after flush. + */ + public void addToFlushQueue(PersistRequestUpdateSql request, boolean early) { + if (early) { + // add it to the early queue + if (earlyQueue == null) { + earlyQueue = new Queue(); + } + earlyQueue.add(request); + } else { + // add it to the late queue + if (lateQueue == null) { + lateQueue = new Queue(); + } + lateQueue.add(request); + } + } + + private static class Queue { + + private final List queue = new ArrayList<>(); + + boolean flush() { + if (queue.isEmpty()) { + return false; + } + for (PersistRequestUpdateSql request : queue) { + request.executeAddBatch(); + } + queue.clear(); + return true; + } + + void add(PersistRequestUpdateSql request) { + queue.add(request); + } + } } diff --git a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 4fc291ca9..9026e17d4 100644 --- a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -141,12 +141,24 @@ public final class DefaultPersister implements Persister { } } + @Override + public void executeOrQueue(SpiSqlUpdate update, SpiTransaction t, boolean queue) { + if (queue) { + addToFlushQueue(update, t, false); + } else { + executeSqlUpdate(update, t); + } + } + + private void addToFlushQueue(SpiSqlUpdate update, SpiTransaction t, boolean early) { + new PersistRequestUpdateSql(server, update, t, persistExecute).addToFlushQueue(early); + } + /** * Execute the updateSql. */ @Override public int executeSqlUpdate(SqlUpdate updSql, Transaction t) { - return executeOrQueue(new PersistRequestUpdateSql(server, (SpiSqlUpdate) updSql, (SpiTransaction) t, persistExecute)); } @@ -958,12 +970,19 @@ public final class DefaultPersister implements Persister { } } - void deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany many, Transaction t, boolean publish) { + void deleteManyIntersection(EntityBean bean, BeanPropertyAssocMany many, SpiTransaction t, boolean publish, boolean queue) { - // delete all intersection rows for this bean + SpiSqlUpdate sqlDelete = deleteAllIntersection(bean, many, publish); + if (queue) { + addToFlushQueue(sqlDelete, t, true); + } else { + executeSqlUpdate(sqlDelete, t); + } + } + + private SpiSqlUpdate deleteAllIntersection(EntityBean bean, BeanPropertyAssocMany many, boolean publish) { IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish); - SqlUpdate sqlDelete = intRow.createDeleteChildren(server); - executeSqlUpdate(sqlDelete, t); + return intRow.createDeleteChildren(server); } /** @@ -1012,7 +1031,7 @@ public final class DefaultPersister implements Persister { if (many.hasJoinTable()) { if (deleteMode.isHard()) { // delete associated rows from intersection table (but not during soft delete) - deleteAssocManyIntersection(parentBean, many, t, request.isPublish()); + deleteManyIntersection(parentBean, many, t, request.isPublish(), false); } } else { diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java index 6d2b5fd3b..37a98d58a 100644 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -1,9 +1,9 @@ package io.ebeaninternal.server.persist; -import io.ebean.SqlUpdate; import io.ebean.bean.BeanCollection; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; +import io.ebeaninternal.api.SpiSqlUpdate; import io.ebeaninternal.server.core.PersistRequest; import io.ebeaninternal.server.core.PersistRequestBean; import io.ebeaninternal.server.deploy.BeanCollectionUtil; @@ -24,7 +24,7 @@ import java.util.Set; /** * Saves the details for a OneToMany or ManyToMany relationship (entity beans). */ -class SaveManyBeans extends SaveManyBase { +public class SaveManyBeans extends SaveManyBase { private static final Logger log = LoggerFactory.getLogger(SaveManyBeans.class); @@ -244,14 +244,29 @@ class SaveManyBeans extends SaveManyBase { if (value == null) { return; } + if (request.isQueueManyIntersection()) { + // queue/delay until bean persist request is flushed + this.deleteMissing = deleteMissingChildren; + request.setManyIntersection(this); + } else { + saveAssocManyIntersection(deleteMissingChildren, false); + } + } + + /** + * Push intersection table changes onto batch flush queue. + */ + public void saveIntersectionBatch() { + saveAssocManyIntersection(deleteMissing, true); + } + + private void saveAssocManyIntersection(boolean deleteMissingChildren, boolean queue) { - //SpiTransaction t = saveManyPropRequest.getTransaction(); boolean vanillaCollection = !(value instanceof BeanCollection); - if (vanillaCollection || deleteMissingChildren) { // delete all intersection rows and then treat all // beans in the collection as additions - persister.deleteAssocManyIntersection(parentBean, many, transaction, publish); + persister.deleteManyIntersection(parentBean, many, transaction, publish, queue); } Collection deletions = null; @@ -287,10 +302,6 @@ class SaveManyBeans extends SaveManyBase { transaction.depth(+1); if (additions != null && !additions.isEmpty()) { - // ensure any cascade batch has been flushed prior - // to inserting into the intersection table - transaction.flushBatch(); - for (Object other : additions) { EntityBean otherBean = (EntityBean) other; // the object from the 'other' side of the ManyToMany @@ -309,24 +320,20 @@ class SaveManyBeans extends SaveManyBase { } else { // build a intersection row for 'insert' IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish); - SqlUpdate sqlInsert = intRow.createInsert(server); - persister.executeSqlUpdate(sqlInsert, transaction); + SpiSqlUpdate sqlInsert = intRow.createInsert(server); + persister.executeOrQueue(sqlInsert, transaction, queue); } } } } if (deletions != null && !deletions.isEmpty()) { - // ensure any cascade batch has been flushed prior - // to inserting into the intersection table - transaction.flushBatch(); - for (Object other : deletions) { EntityBean otherDelete = (EntityBean) other; // the object from the 'other' side of the ManyToMany // build a intersection row for 'delete' IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherDelete, publish); - SqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD); - persister.executeSqlUpdate(sqlDelete, transaction); + SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD); + persister.executeOrQueue(sqlDelete, transaction, queue); } } diff --git a/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTable.java b/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTable.java index 687bccdf8..790a4eb82 100644 --- a/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTable.java +++ b/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTable.java @@ -47,12 +47,11 @@ public class TestOneToManyJoinTable extends BaseTestCase { assertThat(sql.get(1)).contains("insert into troop_monkey (troop_pid, monkey_mid) values (?, ?)"); } - int intersectionRows = Ebean.createSqlQuery("select count(*) as total from troop_monkey where troop_pid = ?") + long intersectionRows = Ebean.createSqlQuery("select count(*) as total from troop_monkey where troop_pid = ?") .setParameter(1, troop.getPid()) - .findOne() - .getInteger("total"); + .findSingleLong(); - assertThat(intersectionRows).isEqualTo(2); + assertThat(intersectionRows).isEqualTo(2L); LoggedSqlCollector.current(); JtTroop fetchTroop = Ebean.find(JtTroop.class) diff --git a/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTableInheritance.java b/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTableInheritance.java index 3de523684..386d83745 100644 --- a/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTableInheritance.java +++ b/src/test/java/org/tests/o2m/jointable/TestOneToManyJoinTableInheritance.java @@ -34,14 +34,15 @@ public class TestOneToManyJoinTableInheritance extends BaseTestCase { List sql = LoggedSqlCollector.current(); - assertThat(sql).hasSize(11); + assertThat(sql).hasSize(8); assertThat(sql.get(0)).contains("insert into class_super "); - assertThat(sql.get(2)).contains("insert into monkey "); - assertThat(sql.get(2)).contains("insert into monkey "); - assertThat(sql.get(5)).contains("insert into class_super_monkey "); - assertThat(sql.get(6)).contains("insert into class_super "); - assertThat(sql.get(8)).contains("insert into monkey "); - assertThat(sql.get(10)).contains("insert into class_super_monkey "); + assertThat(sql.get(1)).contains("-- bind(ClassA)"); + assertThat(sql.get(2)).contains("-- bind(ClassB)"); + assertThat(sql.get(3)).contains("insert into monkey "); + assertThat(sql.get(4)).contains("-- bind(Sim"); + assertThat(sql.get(5)).contains("-- bind(Tim"); + assertThat(sql.get(6)).contains("-- bind(Uim"); + assertThat(sql.get(7)).contains("insert into class_super_monkey (class_super_sid, monkey_mid) values (?, ?)"); ClassA dbA = Ebean.find(ClassA.class, 1); ClassB dbB = Ebean.find(ClassB.class, 2); diff --git a/src/test/java/org/tests/sp/TestManyToManySaveTwice.java b/src/test/java/org/tests/sp/TestManyToManySaveTwice.java index 7c7654e1d..87d28e87a 100644 --- a/src/test/java/org/tests/sp/TestManyToManySaveTwice.java +++ b/src/test/java/org/tests/sp/TestManyToManySaveTwice.java @@ -1,43 +1,107 @@ package org.tests.sp; import io.ebean.BaseTestCase; +import io.ebean.DB; import io.ebean.Ebean; +import io.ebean.Transaction; +import org.junit.Test; import org.tests.sp.model.car.Car; import org.tests.sp.model.car.Wheel; -import org.junit.Assert; -import org.junit.Test; import java.util.LinkedList; import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + public class TestManyToManySaveTwice extends BaseTestCase { + @Test + public void insertBatch() { + + delete(); + + Wheel w0 = new Wheel("wx0"); + Wheel w1 = new Wheel("wx1"); + + List wheels = new LinkedList<>(); + wheels.add(w0); + wheels.add(w1); + + DB.saveAll(wheels); + + Car c0 = new Car("cx0"); + c0.getWheels().add(w0); + c0.getWheels().add(w1); + + Car c1 = new Car("cx1"); + c1.getWheels().add(w1); + + try (Transaction transaction = DB.beginTransaction()) { + transaction.setBatchMode(true); + + DB.save(c0); + DB.save(c1); + + transaction.commit(); + } + } + + + @Test + public void insertBasic() { + + delete(); + + Wheel w0 = new Wheel("wx0"); + Wheel w1 = new Wheel("wx1"); + + List wheels = new LinkedList<>(); + wheels.add(w0); + wheels.add(w1); + + DB.saveAll(wheels); + + Car c0 = new Car("cx0"); + c0.getWheels().add(w0); + c0.getWheels().add(w1); + + Car c1 = new Car("cx1"); + c1.getWheels().add(w1); + + DB.save(c0); + DB.save(c1); + } + @Test public void testInsertCarTwice() { - Ebean.createSqlUpdate("delete from sp_car_car_wheels").execute(); - Ebean.createSqlUpdate("delete from sp_car_wheel").execute(); - Ebean.createSqlUpdate("delete from sp_car_car").execute(); + delete(); List wheels = new LinkedList<>(); - wheels.add(new Wheel()); - wheels.add(new Wheel()); - wheels.add(new Wheel()); - wheels.add(new Wheel()); + wheels.add(new Wheel("w0")); + wheels.add(new Wheel("w1")); + wheels.add(new Wheel("w2")); + wheels.add(new Wheel("w3")); - Car c = new Car(); + Car c = new Car("c1"); c.setWheels(wheels); - Ebean.save(c); // NOTE 1ST SAVE + DB.save(c); // NOTE 1ST SAVE + assertNotNull(c.getId()); - Assert.assertFalse("No ID assigned!", c.getId() == null); - - Ebean.save(c); // NOTE 2ND SAVE + DB.save(c); // NOTE 2ND SAVE List allCars = Ebean.find(Car.class).findList(); - Assert.assertEquals("Inserted 1 car, received more/less!", 1, allCars.size()); + assertEquals(1, allCars.size()); List allWheels = Ebean.find(Wheel.class).findList(); - Assert.assertEquals("Inserted 4 wheels, received more/less!", 4, allWheels.size()); + assertEquals(4, allWheels.size()); + } + + private void delete() { + DB.sqlUpdate("delete from sp_car_car_wheels").execute(); + DB.sqlUpdate("delete from sp_car_wheel").execute(); + DB.sqlUpdate("delete from sp_car_car").execute(); } } diff --git a/src/test/java/org/tests/sp/model/car/Car.java b/src/test/java/org/tests/sp/model/car/Car.java index 8058bd7a4..1c30f5e58 100644 --- a/src/test/java/org/tests/sp/model/car/Car.java +++ b/src/test/java/org/tests/sp/model/car/Car.java @@ -16,10 +16,20 @@ public class Car extends IdEntity { private static final long serialVersionUID = 2579148859565507940L; + private final String name; + @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "sp_car_car_wheels", joinColumns = {@JoinColumn(name = "car")}, inverseJoinColumns = {@JoinColumn(name = "wheel")}) private List wheels; + public Car(String name) { + this.name = name; + } + + public String getName() { + return name; + } + public List getWheels() { return wheels; } diff --git a/src/test/java/org/tests/sp/model/car/Wheel.java b/src/test/java/org/tests/sp/model/car/Wheel.java index 44bf861b0..46d8ad41f 100644 --- a/src/test/java/org/tests/sp/model/car/Wheel.java +++ b/src/test/java/org/tests/sp/model/car/Wheel.java @@ -11,4 +11,13 @@ public class Wheel extends IdEntity { private static final long serialVersionUID = 2399600193947163469L; + private String name; + + public Wheel(String name) { + this.name = name; + } + + public String getName() { + return name; + } }