Consider this scenario:
A Car can have a number of issues
An issue can be shared by a number of cars
I want to define this relationship using a Many-To-Many mapping with a join table.
And I want a generic column MODIFIED_TS to be maintained automatically on all three tables (yes - I can do it with database triggers - but that is not my ambition).
I have experimented with a variety of hibernate entity listeners and interceptors. They work fine - except for the join table.
Is there anyone out there who knows why callbacks like e.g. preInsert and onSave do not fire when rows are inserted or updated on the join table?
I am using:
Hibernate 4.3.10.Final
spring-data-jpa 1.8.1.RELEASE (does it matter?)
Base entity class:
#MappedSuperclass
public abstract class BaseEntity {
#Column(name = "MODIFIED_TS")
private Timestamp modifiedTs;
}
Car:
#Entity
#Table(name = "CAR")
public class Car extends BaseEntity {
#Id
#Column(name = "ID", nullable = false)
private long id;
#Column(name = "LICENSE_PLATE_NUMBER", nullable = false, length = 20)
private String licensePlateNumber;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "CAR_ISSUE", inverseJoinColumns = {#JoinColumn(name = "CAR_ID")},
joinColumns = {#JoinColumn(name = "ISSUE_ID")})
private Collection<Issue> issues;
}
Issue:
#Entity
#Table(name = "ISSUE")
public class Issue extends BaseEntity {
#Id
#Column(name = "ID", nullable = false)
private long id;
#Column(name = "TEXT", nullable = false, length = 2000)
private String text;
}
and CarIssue:
#Entity
#Table(name = "CAR_ISSUE")
public class CarIssue extends BaseEntity {
#Id
#Column(name = "ISSUE_ID", nullable = false)
private long issueId;
#Id
#Column(name = "CAR_ID", nullable = false)
private long carId;
}
My listener:
public class MyEntityListener implements PreUpdateEventListener, PreInsertEventListener {
public boolean setAudit(Object entity, String[] properties, Object[] state) {
if (entity instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) entity;
baseEntity.setModifiedTs(new Timestamp(new Date().getTime()));
List<String> propertiesList = Arrays.asList(properties);
state[propertiesList.indexOf("modifiedTs")] = baseEntity.getModifiedTs();
}
return false;
}
#Override
public boolean onPreInsert(PreInsertEvent event) {
return setAudit(event.getEntity(), event.getPersister().getPropertyNames(), event.getState());
}
#Override
public boolean onPreUpdate(PreUpdateEvent event) {
return setAudit(event.getEntity(), event.getPersister().getPropertyNames(), event.getState());
}
}
and finally this piece of code:
Car car = new Car();
car.setLicensePlateNumber("ABC123");
ArrayList<Issue> issues = new ArrayList<>();
Issue issue = new Issue();
issue.setDescription("Two wheels missing");
issues.add(issue);
car.setIssues(issues);
carRepo.save(car);
This works fine - I get a row in all three tables - but MODIFIED_TS only gets populated for CAR and ISSUE.
Any help is greatly appreciated:-)
Related
I have a table that holds events where the data is stored as a JSON object.
What i'am trying to do is have different entities mapped to the same table, because some of these events need to join with other tables to be useful.
What i tried doing:
EntityA is the "default" entity for the table, all events are received in this entity an them stored.
#Entity(name = "entity_a")
#Table(name = "table_a")
public class EntityA {
#Id
private UUID id;
#Column(name = "fielda")
private String fieldA;
#Column(name = "fieldb")
private Integer fieldB;
#Column(name = "json_field")
private Object jsonField;
}
EntityAB is a map of the same table with a new field that is a join with another table.
#Entity(name = "entity_ab")
#Table(name = "table_a")
public class EntityAB {
#Id
private UUID id;
#Column(name = "fielda")
private String fieldA;
#Column(name = "fieldb")
private Integer fieldB;
#Column(name = "json_field")
private Object jsonField;
#ManyToOne
#ManyToOne
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula = #JoinFormula(value = "JSON_VALUE(jsonField, '$.field_id_a')", referencedColumnName = "field_id_a")),
#JoinColumnOrFormula(formula = #JoinFormula(value = "JSON_VALUE(jsonField, '$.field_id_b')", referencedColumnName = "field_id_b")),
#JoinColumnOrFormula(formula = #JoinFormula(value = "JSON_VALUE(jsonField, '$.fieldc')", referencedColumnName = "fieldc")),
})
private EntityB entityB;
}
EntityB is used in the EntityAB.
public class EntityB implements Serializable {
#EmbeddedId
private EntityBId id;
#Column(name = "fieldc")
private String fieldC;
}
#Embeddable
public class EntityBId implements Serializable {
#Column(name = "field_id_a")
private String fieldIdA;
#Column(name = "field_id_b")
private Integer fieldIdB;
#Column(name = "field_id_c")
private Integer fieldIdC;
}
When starting the application, it gives me this error:
referencedColumnNames(field_id_a, field_id_b, fieldc) of EntityAB.entityB referencing EntityB not mapped to a single property
If i dont use the fields "field_id_a" and "field_id_b" (EmbeddedId) in the join, it works, but it's not the right join. From what i have searched it looks like i can't have a parcial PK join when using "EmbeddedId", is that correct?
I have a JPA/Hibernate mapping as follows, in one of my projects where I use spring-data-jpa in the persistence layer.
#Entity
public class SalesOrder {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
#OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL)
#JoinColumn(name = "salesOrderId", referencedColumnName = "id", nullable = false)
List<OrderLine> lines = new ArrayList<>();
//...
}
#Entity
#IdClass(SalesOrderLinePK.class)
public class SalesOrderLine {
#Id
String sku;
#Id
#Column(name = "salesOrderId", insertable = false, updatable = false)
Integer salesOrderId;
//...
}
public class SalesOrderLinePK implements Serializable {
String sku;
Integer salesOrderId;
OrderLinePK() {}
OrderLinePK(String sku, Integer salesOrderId) {
this.sku = sku;
this.salesOrderId= salesOrderId;
}
}
When I persist a new Order entity with some new OrderLine children, it gives me an error due to the reason salesOrderId field of SalesOrderLine is not set, instead, be null.
The following is the particular test case which fails with the error.
#Test
void create() {
SalesOrder item = randomItem(false);
item = salesOrderService.create(item);
assertNotNull(item);
assertNotNull(item.getId());
assertNotNull(item.getVersion());
}
private SalesOrder randomItem(boolean persistant) {
String random = RandomString.make(5);
SalesOrder order = new SalesOrder();
order.setOrderDate(LocalDate.now());
order.setOrderState(OrderState.DONE);
for(int i = 0; i < 5; i++) {
SalesOrderLine item = new SalesOrderLine();
item.setIdx(i);
item.setCode(random);
item.setName("Catalog Item " + random);
item.setListedPrice(BigDecimal.valueOf(120.50));
item.setDiscount(BigDecimal.valueOf(10));
item.setSellingPrice(BigDecimal.valueOf(108.50));
order.getOrderLines().add(item);
}
if (persistant) {
order = salesOrderRepository.save(order);
}
return order;
}
I cannot figure out what I'm doing wrong here. I don't want it to make bidirectional either. Could you please help me solve this issue.
In my spring boot project, I have one LineItem entity below is the code
#Entity
#Table(name = "scenario_lineitem")
#Data
#NoArgsConstructor
public class LineItem implements Cloneable {
private static Logger logger = LoggerFactory.getLogger(GoogleConfigConstant.class);
#Id
#GeneratedValue(strategy = IDENTITY)
private BigInteger lineItemId;
#Column
private String name;
#OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL, CascadeType.PERSIST, CascadeType.MERGE })
#JoinColumn(name = "line_item_meta_id")
private List<QuickPopValue> quickPopValues;
}
Another entity is
#Entity
#Table(name = "quick_pop_value")
#Data
#NoArgsConstructor
public class QuickPopValue implements Cloneable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "quick_pop_value_id", columnDefinition = "bigint(20)", unique = true, nullable = false)
private BigInteger quickPopValueId;
#Column(name = "column_name")
private String columnName;
#Column(name = "value")
private String value;
#Column(name = "formula", columnDefinition = "longtext")
private String formula;
}
Now I am trying to delete QuickPopValue one by one but it's not getting deleted and not getting any exception as well.
Below is the delete code :
List<QuickPopValue> quickPopValues = sheetRepository.findByColumnName(columnName);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.delete(qpValue);
}
Such behavior occurs when deleted object persisted in the current session.
for (QuickPopValue qpValue : quickPopValues) {
// Here you delete qpValue but this object persisted in `quickPopValues` array which is
quickPopValueRepository.delete(qpValue);
}
To solve this you can try delete by id
#Modifying
#Query("delete from QuickPopValue t where t.quickPopValueId = ?1")
void deleteQuickPopValue(Long entityId);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.deleteQuickPopValue(qpValue.getQuickPopValueId());
}
Ok so today I have spent all day trying to figure out how to map a relationship between Orders, Products, and OrderLine with hibernate using annotations all java config.
I want OrderLine to be a junction table with extra fields, that joins Products and Orders.
Orders has a OneToMany relationsbhip with OrderLine - One order has many order lines.
Each Order has one or more OrderLines, Each OrderLine has one Order, Each OrderLine has one Product, Each Product can have 0 or more OrderLines.
From what I have been following along with tutorials, there are two ways to do it. One being with #Embeddable, #EmbeddedId annotations and the other being #IdClass annotations, I have tried both with no success I will post my code for my shot at #IdClass method.
My orders class
#Entity
#Table(name="orders")
public class Order {
#Id
#Column(name = "order_id")
#OneToMany(fetch = FetchType.EAGER, mappedBy = "order")
private Set<OrderLine> orderLines = new HashSet<>();
...some extra properties relevant to all orders
public Order(){}
...setters/getters
}
My Products class
#Entity
#Table(name="products")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "product_id")
public Integer product_id; // primary key
#OneToMany(fetch = FetchType.EAGER, mappedBy = "product")
private Set<OrderLine> orderLines = new HashSet<>();
...other properties
public Product() {
}
...setters/getters
}
Here is my OrderLine Class
#Entity
#IdClass(OrderLineId.class)
#Table(name="order_line")
public class OrderLine implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "product_id", insertable= false, updatable= false)
public Integer product_id;
#Id
#Column(name = "order_id", insertable= false, updatable= false)
public Integer order_id;
#ManyToOne
#JoinColumn(name = "product_id", insertable = false, updatable = false)
private Product product;
#ManyToOne
#JoinColumn(name = "order_id", insertable = false, updatable = false)
private Order order;
#Column(name = "product_quantity", unique = false, nullable = false)
private int productQuantity;
...additional fields associated with each OrderLine
And finally my OrderLineId implementation
public class OrderLineId implements Serializable {
private static final long serialVersionUID = 1L;
private Integer order_id;
private Integer product_id;
public OrderLineId(){}
public OrderLineId(Integer order_id, Integer product_id) {
this.order_id = order_id;
this.product_id = product_id;
}
#Column(name = "order_id")
public Integer getOrder() {
return this.order_id;
}
public void setOrder(Integer order_id) {
this.order_id = order_id;
}
#Column(name = "product_id")
public Integer getProduct() {
return this.product_id;
}
public void setProduct(Integer product_id) {
this.product_id = product_id;
}
#Override
public int hashCode() {
return order_id + product_id;
}
#Override
public boolean equals(Object obj) {
if(obj instanceof OrderLineId){
OrderLineId orderLineId = (OrderLineId) obj;
return orderLineId.order_id == order_id && orderLineId.product_id == product_id;
}
return false;
}
}
Here is the exception I am getting
MySQLSyntaxErrorException: Unknown column 'orderlines0_.order' in 'field list'
when I visit the end point below
#RequestMapping(value = "/testorder", method = RequestMethod.GET)
public ModelAndView testOrder(){
Order order = orderService.findOrderById(23);
Product product = productService.findProduct(2);
OrderLine ol = new OrderLine(product, order);
ol.setSize("large");
ol.setProductQuantity(30);
orderService.saveOrderLine(ol);
return null;
}
Please can somebody help me, this is driving me crazy....
Thank you
After going through your question and code, I think you have got your design wrong.
You say
want OrderLine to be a junction table with extra fields, that joins Products and Orders.
However you have only two variables, Order and Product in your OrderLine Class.
If I am not wrong, What you really need is a many-to-many table.
Your table order_line would contain two columns order_id and product_id, having foreign key to table orders and products respectively.
Your Order class would be:
#Entity
#Table(name="orders")
public class Order {
#Id
//id of table Order
#ManyToMany(
targetEntity = Product.class,
cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
#JoinTable(name = "order_line",
joinColumns =
#JoinColumn(name = "order_id", nullable = false, updatable = false),
inverseJoinColumns =
#JoinColumn(name = "product_id", nullable = false, updatable = false))
public Set<Product> products= new HashSet(0);
...some extra properties relevant to all orders
public Order(){}
...setters/getters
}
Your Product Class would look like:
#Entity
#Table(name="products")
public class Product implements Serializable {
#Id
#Column(name = "product_id")
public Integer product_id; // primary key
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "products", targetEntity = Order.class) private Set<Order> order = new HashSet<>();
...other properties
public Product() {
}
...setters/getters
}
As you can see, from Order entity you can get 'products' and from Product entity you can get 'orders'. This should server your purpose. No need to use 'OrderLine' class.
Other major errors in code:
- #Id is used to represent Primary key. You have used #Id multiple times for single class.
- In 'Order' class #Id given to Set, and #Id is used along with #OneToMany, which won't work.
Code provided would help you if, indeed what you need is many-to-many table.
I got it to work on my last try last night by doing this change
in OrderLine I removed the product_id and order_id fields and placed the #Id annotation over both ManyToOne relationships like so
#Entity
#IdClass(OrderLineId.class)
#Table(name="order_line")
public class OrderLine implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#ManyToOne
#JoinColumn(name = "product_id", insertable = false, updatable = false)
private Product product;
#Id
#ManyToOne
#JoinColumn(name = "order_id", insertable = false, updatable = false)
private Order order;
#Column(name = "product_quantity", unique = false, nullable = false)
private int productQuantity;
...additional fields associated with each OrderLine
I did not think this woudl work but correct table with correct values was updated, is this valid?
Thank you
Ok I figured it out - I ended up using embeddable method instead based on this guys excellent tutorial here
He goes through 3 major ways to implement many to many assosiations - a simple junction only table, a junction table (with additional fields) which is the method I needed to implement, and finally a less popular method where the class is mapped as a component.
I'm running into an issue with adding JOIN's to a subquery using DetachedCriteria. The code looks roughly like this:
Criteria criteria = createCacheableCriteria(ProductLine.class, "productLine");
criteria.add(Expression.eq("productLine.active", "Y"));
DetachedCriteria subCriteria = DetachedCriteria.forClass(Model.class, "model");
subCriteria.setProjection(Projections.rowCount());
subCriteria.createAlias("model.modelLanguages", "modelLang");
subCriteria.createAlias("modelLang.language", "lang");
criteria.add(Expression.eq("lang.langCode", "EN"));
subCriteria.add(Restrictions.eqProperty("model.productLine.productLineId","productLine.productLineId"));
criteria.add(Subqueries.lt(0, subCriteria));
But the logged SQL does not contain the JOIN in the subquery, but does include the alias which is throwing an error
SELECT *
FROM PRODUCT_LINE this_
WHERE this_.ACTIVE=?
AND ? <
(SELECT COUNT(*) AS y0_
FROM MODEL this0__
WHERE lang3_.LANG_CODE ='EN'
AND this0__.PRODUCT_LINE_ID =this_.ID
)
How can I add the joins to the DetachedCriteria?
#Entity
#Table(name = "PRODUCT_LINE")
public class ProductLine implements java.io.Serializable {
private long productLineId;
private char active;
private Set<Models> models = new HashSet<Models>(0);
#OneToMany(fetch = FetchType.LAZY, mappedBy = "productLine")
public Set<Models> getModels() {
return this.models;
}
}
#Entity
#Table(name = "MODEL")
public class Model implements java.io.Serializable {
private long modelId;
private ProductLine productLine;
private String name;
private Set<ModelLanguages> modelLanguages = new HashSet<ModelLanguages>(0);
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "PRODUCT_LINE_ID")
public ProductLine getProductLine() {
return this.productLine;
}
#Column(name = "NAME", nullable = false)
public String getName() {
return this.name;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "model")
public Set<ModelLanguages> getModelLanguages() {
return this.modelLanguages;
}
}
#Entity
#Table(name = "MODEL_LANGUAGES")
public class ModelLanguages implements java.io.Serializable {
private long id;
private Language language;
private Model model;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "LANGUAGE_ID", nullable = false, insertable = false, updatable = false)
public Language getLanguage() {
return this.language;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "MODEL_ID", nullable = false, insertable = false, updatable = false)
public Model getModel() {
return this.model;
}
}
#Entity
#Table(name = "LANGUAGES", uniqueConstraints = #UniqueConstraint(columnNames = "LANG_CODE"))
public class Language implements java.io.Serializable {
private long languageId;
private String langCode;
private Set<ModelLanguages> modelLanguages = new HashSet<ModelLanguages>(
0);
#Column(name = "LANG_CODE", unique = true, nullable = false)
public String getLangCode() {
return this.langCode;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "language")
public Set<ModelLanguages> getModelLanguages() {
return this.modelLanguages;
}
}
Hibernate version: 3.2.6.ga
Hibernate core: 3.3.2.GA
Hibernate annotations: 3.4.0.GA
Hibernate commons-annotations: 3.3.0.ga
Hibernate entitymanager: 3.4.0.GA
Hibernate validator: 3.1.0.GA
Don't you have a typo in your code at the following line :
criteria.add(Expression.eq("lang.langCode", "EN"));
I think, you should add this restriction on the subcriteria and not the criteria.
If you need to join tables in subquery can you try make this. It is explicitly specifying joinType in your detached criteria.
subCriteria.createAlias("model.modelLanguages", "modelLang", CriteriaSpecification.LEFT_JOIN);
subCriteria.createAlias("modelLang.language", "lang", CriteriaSpecification.LEFT_JOIN);