I have some entity classes which have a one-to-many - many-to-one relationship. I am using Spring and Hibernate.
Each TwoWayService has exactly 2 Services in my application.
Excerpts:
#Entity
#Table(name = "two_way_services")
public class TwoWayService {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column
private String name;
#OneToMany(cascade = CascadeType.ALL,
mappedBy = "twoWayService",
fetch = FetchType.EAGER)
private List<Service> services;
public TwoWayService() {
services = new ArrayList<>();
// Add two as default
services.addAll(Arrays.asList(new Service(), new Service()));
}
public void setService1(Service service) {
services.set(0, service);
service.setTwoWayService(this);
}
public void setService2(Service service) {
services.set(1, service);
service.setTwoWayService(this);
}
...
}
#Entity
#Table(name = "services")
public class Service {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column
private String name;
#ManyToOne(optional = false)
#JoinColumn
private TwoWayService twoWayService;
public void setTwoWayService(TwoWayService twoWayService) {
this.twoWayService = twoWayService;
}
...
}
I am using Derby on the backend. The database schema is like this:
CREATE TABLE two_way_services (
id INT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),
config_name VARCHAR(255) NOT NULL,
name VARCHAR(80),
admin_ip VARCHAR(32) NOT NULL,
connection_state INT NOT NULL
);
CREATE TABLE services (
id INT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),
name VARCHAR(80),
type INT NOT NULL,
ruleset VARCHAR(255) NOT NULL,
two_way_service_id INT,
FOREIGN KEY (two_way_service_id) REFERENCES two_way_services(id) ON DELETE CASCADE
);
The repository interface:
public interface TwoWayServiceRepository extends Repository<TwoWayService, Integer> {
<S extends T> S save(S entity);
...
}
In my unit tests, I find that when I call findOne on a TwoWayService, I find that I have 4 Services instead of 2. Browsing the database directly shows the data as I would expect.
TwoWayService tws1 = repo.findOne(1); // get by id
assertThat(tws1.getServices().size()).isEqualTo(2); // fails, expected:<[2]> but was:<[4]>
Examining it in the debugger I see 4 elements in the services list: the two that I expect, plus 2 extra ones which are copies of the expected. I don't see where these are coming from. Why are these extra objects appearing in the list?
I am not sure but I think, it is because you add 2 services in the constructor and 1 in each setter. This makes 4 in total. You test for the amount of services, is that what you wanted to test?
Related
Hi I have couple of Entity classes as below, using lombok for getter and setters
Parent Entity Class have
#Table(name = "PARTY")
#Entity
public class Party {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "PARTY_ID")
#MapKey(name="dvpParticipantName")
#LazyCollection(LazyCollectionOption.FALSE)
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
}
Child Entity Class have
#Table(name = "DV_LOAN_PARTICIPANTS")
#Entity
public class DVLoanParticipants implements Serializable {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
}
In service class i am calling save operation as
repository.save(parentEntityObject);
I am able to execute update statements ,but when i try to insert new row for child entity class i am getting an error saying
cannot insert NULL into ("ABC"."DV_LOAN_PARTICIPANTS"."PARTY_ID")
But if i print the parentEntityObject just before the save operation i see the values like
(partyId=12345678, dvpParticipantName=XYZ, dvpParticipantType=VKP)
I see the query formed as
insert
into
DV_LOAN_PARTICIPANTS
(DVP_PARTICIPANT_TYPE, PARTY_ID, DVP_PARTICIPANT_NAME)
values
(?, ?, ?)
Just before te save i am seeing valules in the Object
Builder=DVLoanParticipants(partyId=123456, dvpParticipantName=Builder,
dvpParticipantType=Individual)
Update
This is the setting part for values
DVLoanParticipants dvLoanParticipants = new
DVLoanParticipants();
dvLoanParticipants.setPartyId(Long.valueOf(partyId));
dvLoanParticipants.setDvpParticipantName("Builder");
dvLoanParticipants.setDvpParticipantType("Individual");
Party party = new Party();
Map<String, DVLoanParticipants> dvLoanParticipantsMap = new
java.util.HashMap<>();
dvLoanParticipantsMap.put("Builder", dvLoanParticipants);
party.setPartyId(Long.valueOf(partyId));
party.setDvLoanParticipantsMap(dvLoanParticipantsMap);
repository.save(party);
What is the mistake i am doing ?
The root cause of your problem in this part:
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "LOAN_ID")
#MapKey(name="dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
actually for your case the column name in the #JoinColumn means:
If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the foreign key is in the table of the target entity.
So, assuming for the clarity that you want to map the following schema:
create table PARTY
(
PARTY_ID int,
-- ...
primary key (PARTY_ID)
);
create table DV_LOAN_PARTICIPANTS
(
PARTY_ID int,
DVP_PARTICIPANT_NAME varchar(50),
DVP_PARTICIPANT_TYPE varchar(10),
-- ...
primary key (PARTY_ID, DVP_PARTICIPANT_NAME),
foreign key (PARTY_ID) references PARTY(PARTY_ID)
);
You can use the following mapping:
#Entity
#Table(name = "PARTY")
public class Party
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
// I use fetch = FetchType.EAGER instead of deprecated #LazyCollection(LazyCollectionOption.FALSE)
// targetEntity = DVLoanParticipants.class is redundant here
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "PARTY_ID") // this is DV_LOAN_PARTICIPANTS.PARTY_ID column
#MapKey(name = "dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
public Party()
{
dvLoanParticipantsMap = new HashMap<>();
}
// getters / setters
public void addParticipant(DVLoanParticipants p)
{
this.dvLoanParticipantsMap.put(p.getDvpParticipantName(), p);
p.setPartyId(getPartyId());
}
}
#Entity
#Table(name = "DV_LOAN_PARTICIPANTS")
public class DVLoanParticipants implements Serializable
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
// getters / setters
}
and example how to save:
Party party = new Party();
party.setPartyId(2L);
// ...
DVLoanParticipants part1 = new DVLoanParticipants();
part1.setDvpParticipantName("Name 3");
part1.setDvpParticipantType("T1");
DVLoanParticipants part2 = new DVLoanParticipants();
part2.setDvpParticipantName("Name 4");
part2.setDvpParticipantType("T1");
party.addParticipant(part1);
party.addParticipant(part2);
repository.save(party);
and several notes:
The LazyCollectionOption.TRUE and LazyCollectionOption.FALSE values are deprecated since you should be using the JPA FetchType attribute of the #OneToMany association.
You use hibernate specific approach for mapping сomposite identifiers. As it's mentioned in the hibernate documentation:
The restriction that a composite identifier has to be represented by a primary key class (e.g. #EmbeddedId or #IdClass) is only JPA-specific.
Hibernate does allow composite identifiers to be defined without a primary key class via multiple #Id attributes.
But if you want to achieve more portability you should prefer one of the jpa allowed approaches.
I am new to spring boot and was trying to implement an sales related application. There i implemented a custom findBy method using property name customer id . The following is the output i am getting
[{"customerId":101,"stockId":1},{"customerId":101,"stockId":1},{"customerId":101,"stockId":1},{"customerId":101,"stockId":1}]
which is actually the first row but the values in table is different.
CUSTOMER_ID STOCK_ID
101 1
101 2
101 3
101 4
102 1
103 2
103 3
104 4
Since it is a sample start up application i didnt specify any constraints. what i am doing wrong here.
Update:
this is my Repository
#Repository
public interface CustomerSalesRepo extends JpaRepository<CustomerSales,Integer> {
public List<CustomerSales> findAllByCustomerId(int custID);
}
this is my sales entity
#Entity
#Table(name = CustomerSales.TABLE_NAME)
public class CustomerSales {
public static final String TABLE_NAME= "CUSTOMER_SALES";
#Id
#Column(name="CUSTOMER_ID")
private int customerId;
#Column
private int stockId;
data.sql
DROP TABLE CUSTOMER_SALES IF EXISTS;
CREATE TABLE CUSTOMER_SALES (customer_Id INT, stock_Id INT);
INSERT INTO CUSTOMERSALES (customer_Id,stock_Id)VALUES (101,1),(101,2),(101,3),(101,4),(102,1),(103,2),(103,3),(104,4);
I am not sure how you were able to populate that data set into your DB table since there is a primary key on customerId only. That means that you cannot have more than 1 row with CUSTOMER_ID = 101.
You should define a composite PK class
public class CustomerSalesPk implements Serializable
{
private int customerId;
private int stockId;
public int getCustomerId()
{
return customerId;
}
public void setCustomerId(final int customerId)
{
this.customerId = customerId;
}
public int getStockId()
{
return stockId;
}
public void setStockId(final int stockId)
{
this.stockId = stockId;
}
}
Then use this PK class in your entity
#Entity
#Table(name = CustomerSales.TABLE_NAME)
#IdClass(CustomerSalesPk.class)
public class CustomerSales
{
public static final String TABLE_NAME = "CUSTOMER_SALES";
#Id
#Column(name = "CUSTOMER_ID")
private int customerId;
#Id
#Column(name = "STOCK_ID")
private int stockId;
I am running the latest version of Hibernate with 2 entities: Project and ProjectShare, which have a one-to-many relation set up bidirectional.
A ProjectShare is uniquely identified by a composite ID containing project_id and user_id. Besides the key, a ProjectShare contains a boolean flag whether the user gets read or write access on the project.
#Entity
#Table(name = "projects")
public class Project {
#Id
#GeneratedValue // UUID generator
#Column(name = "project_id")
private String id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProjectShare> shares = new ArrayList<>();
public Project(String name) {
this.name = name;
}
public void addShare(ProjectShare share) {
shares.add(share);
share.setProject(this);
}
public void removeShare(ProjectShare share) {
shares.remove(share);
share.setProject(null);
}
}
#Entity
#Table(name = "project_shares")
public class ProjectShare {
#EmbeddedId
private ProjectShareId id;
#Column(name = "has_write_access")
private boolean hasWriteAccess;
#MapsId("projectId")
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "project_id", nullable = false, updatable = false)
private Project project;
public ProjectShare(ProjectShareId id, boolean hasWriteAccess) {
this.id = id;
this.hasWriteAccess = hasWriteAccess;
}
public void setProject(Project project) {
this.project = project;
}
}
#Embeddable
public class ProjectShareId implements Serializable {
#Column(name = "project_id")
private String projectId;
#Column(name = "user_id")
private String userId;
public ProjectShareId(String userId) {
this.userId = userId;
}
// equals and hashCode go here...
}
If I create a new Project and assign new ProjectShare associations to it, everything works fine:
Project project = new Project("my_project");
project.addShare(new ProjectShare(new ProjectShareId("user1"), false));
project.addShare(new ProjectShare(new ProjectShareId("user2"), false));
projectRepository.save(project); // assume project id is '123'
Since all objects are new and not yet persisted, it executes 1 insert for the Project itself and 1 insert for each ProjectShare. Lets assume the project is inserted with id '123'.
Now, if I load this existing project, and add new ProjectShares to it, things go wrong:
Project project = projectRepository.findById("123");
project.addShare(new ProjectShare(new ProjectShareId("user2"), true));
project.addShare(new ProjectShare(new ProjectShareId("user3"), true));
projectRepository.save(project);
For every ProjectShare, this executes a SELECT on the values in ProjectShareId (project_id and user_id), followed by either an INSERT of UPDATE, depending on whether the record was found. This is the basic merge strategy of Hibernate and this is what we want.
The desired outcome of this should be:
Leave ProjectShare for user1 untouched
Update the ProjectShare for user2 (from false to true)
Create a new ProjectShare for user3
However, when the SELECT is executed for the ProjectShare, the foreign key project_id is always null. This means that existing records are never found, an INSERT is attempted instead of and UPDATE, and a DB-level Constraint violation is triggered.
How should I solve this issue? Should I manually go through the project.getShares() collection, find existing records and update them instead? I was hoping Hibernate would do this through its merge strategy.
Or could this be a bug in Hibernate related to associations with foreign keys in Embbedded IDs?
I faced the issue described in the title saving my entity though everything in code and db tables seems ok.
Here is my code:
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "company_id", referencedColumnName = "id")
List<CompanyObject> companyObjects;
}
#Entity
public class CompanyObject {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#Enumerated(EnumType.STRING)
ObjectType type;
}
Here is my table definitions:
CREATE TABLE `company` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8
CREATE TABLE `company_object` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`company_id` bigint(20) NOT NULL,
`type` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
KEY `FK__company` (`company_id`),
CONSTRAINT `FK__company` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Object I'm trying to save contains the following info:
Company(id=32, companyObjects=[CompanyObject(id=null, type=NEW)])
Here is the code I use to save the object:
Controller method:
#RequestMapping(value = "/company/{id}", method = RequestMethod.POST)
public String editCompany(#PathVariable("id") long companyId,
#ModelAttribute("form") CompanyDto form) {
Company company = companyService.getCompanyById(companyId);
companyService.updateCompany(company, form);
return "redirect:/companies";
}
Service method:
#Transactional
public Company updateCompany(Company company, final CompanyDto form) {
company.getCompanyObjects().clear();
company.getCompanyObjects().addAll(form.getCompanyObjects());
return companyRepository.save(company);
}
Am I getting this right that hibernate automatically generate and populate all the missing ids in these objects? If yes what am I missing and why the error appears?
You have some problems here. Firstly, your table definitions are wrong, check your accreditation_object table, you have a foreign key there which references a column that doesn't exist: company_id should be accreditation_company_id.
(or is it just some copy-paste error?)
Secondly, your entities are wrong, try this way:
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#OneToMany(mappedBy="company", cascade = CascadeType.ALL)
Set<CompanyObject> companyObjects;
}
#Entity
public class CompanyObject {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#Enumerated(EnumType.STRING)
ObjectType type;
#ManyToOne
#JoinColumn(name = "company_id")
Company company;
}
Note the #ManyToOne annotation in the CompanyObject. If I understand correctly, you want to assign one or more CompanyObject to a Company, thus you have to have a #ManyToOne annotated Company typed field in the CompanyObject.
Now, if you want to save these objects, first save the Company instance, then iterate over the list of CompanyObjects, set the previously saved company instance, and then save the CompanyObjects, something like this:
Company company = new Company();
companyDao.persist(company);
List<CompanyObject> companyObjects = new ArrayList<>();
// populate the list somehow
// ...
for(CompanyObject obj: companyObjects){
obj.setCompany(company);
companyObjectDao.persist(obj);
}
Your updateCompany method is wrong, you should try something like the above code. (I cannot rewrite your example because it looks like something is missing there. What is CompanyDTO?)
Edit: You can use cascade saving (note: I've updated the Company entity), just be sure to set the Company instance to every CompanyObject instance, like:
#Transactional
public Company updateCompany(Company company, final CompanyDto form) {
company.getCompanyObjects().clear();
company.getCompanyObjects().addAll(form.getCompanyObjects());
for(CompanyObject obj : company.getCompanyObjects()){
obj.setCompany(company);
}
return companyRepository.save(company);
}
I think this should work, but I'm not a 100% sure. Give it a try.
I wanna create a compound primary key, one field of which is autogenerated (JPA).
Can I do this ? Is it possible some how to get a generator from spring environment ?
My entity code is
#Entity
#IdClass(value = IntemKey.class)
public class Item implements Serializable {
#Id // wanna autogenerate this field
private Long id;
#Id
private Date add;
private Date del;
....
getter/setter
}
#Embeddable
public class ItemKey implements Serializable {
private Long id;
private Date add = new Date();
...
getter/setter + equals + hashCode
}
table with data
id add del
1 2015.01.01 null
2 2015.01.03 2015.01.05
2 2015.01.05 null
It can be created by Hibernate, check here for more information.
#Id // wanna autogenerate this field
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
*EDIT : * For compound key check this blog post