I have a resource ,say a Book.
I want the book to have a book id and version id
On Create operations i want to have incremental id
book id version id status
1 0 ACTIVE
2 0 ACTIVE
3 0 ACTIVE
On update i want to have a new version for same id
book id version id status
1 0 INACTIVE //Changed to inactive
1 1 ACTIVE //new row with same id
2 0 ACTIVE
3 0 ACTIVE
i have two tables to achive this
A table to generate the id
CREATE TABLE `BookIdGenerator` (
`id`int(11) NOT NULL AUTO_INCREMENT ,
PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
The Book table
CREATE TABLE `Book` (
`id` int(11) NOT NULL ,
`version` int(11) NOT NULL,
`title` varchar(255) NOT NULL,
`isbn` varchar(255) NOT NULL,
PRIMARY KEY (`id`,`version`),
foreign key (`id`) references BookIdGenerator(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
The entity class is
class Book{
private long id;
private int version;
//other fields
What would be the correct table structure and JPA annotations to achive this?
Without having any separate table I can have #Id at id and version but this does not allow to retrieve the assigned id to an object on persisting it.
update : I can have an Emeddable BookId class(with book id and version) and can use an #EmbeddedId in Book but I have to write a Id generator to generate the id.
What can I do so that i don't have to write a generator to assign id before persisting in create method ?
JPA #Version
Your intended use of the version column is not as intended in JPA.
A version field is used to support the Optimistic Locking of individual DB records. Each time a transaction attempts to update a record, the version field is compared with the value in the DB. If they are the same then no other transaction has updated the record. The record will be updated and the version column changed. If they are not the same then some other transaction has updated the record and an OptimisticLockException will be thrown.
When updating entities (with JPA's merge()), JPA updates the existing record.
In your question, when you ‘update’ the book record, you are in fact creating(persisting) a new record.
Further, the version field is managed by JPA and should not be touched by the application – read only. In your example you are creating a new record with a specified version number which is not allowed.
Primary Keys
You have a PrimaryKey defined for Book, however, you require multiple records to hold the same PrimaryKey. This will fail as the PrimaryKey must be unique.
In this case your id and version form a Compound Primary Key, and you’ll need to separate these fields out into a separate class; a Primary Key Class.
You would require an application specific versioning if you really want to record each change to a record as a separate record, as you can't use the JPA version field for your purposes.
Book_id Book_Update_id status JPA_Version
1 0 INACTIVE 0
1 1 ACTIVE 0
2 0 ACTIVE 0
3 0 ACTIVE 0
I’ve left a JPA version column in although this is not required if you are not modifying records.
so your Book will look something like this.
#Entity //Tell JPA this is an entity to be mapped to your DB.
#IdClass(BookId.class) //you need to repeat the PK fields in this class
class Book{
#Id #Column(name=”Book_id”) //Both id and bookUpdateId annotated with #ID
private long id; //Together they for a compound Primary Key
#Id #Column(name=”Book_Update_id”)
String bookUpdateId
#Version #Column(name=”JPA_Version”) //Tell JPA where to record its version info
private int version;
It will be upto the application, when creating new records, to decide what the id and bookUpdateId values are to be based on the existing values of a record. There is no way JPA can know this. There is therefore no separate table required to support ID generation.
See Pro JPA 2 by Mike Keith and Merrick Schincariol for all these topics, a good introduction and beyond. This will aslo explain the #IdClass in more detail.
Related
I'm using Hibernate to create SQLite tables.
I have a table as such
#Entity
class Person(
#Column(unique = true, nullable = false)
val name: String,
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int? = null,
)
I see that when the database is created, the unique constraint is added later on via an ALTER request
Hibernate: create table Person (id integer, name varchar(255) not null, primary key (id))
Hibernate: alter table Person add constraint UK_is4vd0f6kw9sw4dxyie5cy9fa unique (name)
Except that SQLite does not seem to support ALTER requests modifying constraints on tables.
So my question is : Is there a way to literally indicate Hibernate to set that uniqueness constraint on table creation? What would be the best way to do it?
I can ensure uniqueness easily later on via code, but I'd rather use the power of the database if I can.
I should add that this is for a personal small application so so far I'm using the update setting for hibernate.hbm2ddl.auto so Hibernate generates the SQL itself. I'm open to other methods but I'd rather avoid them if I can to reduce maintenance.
Gonna answer my own question given that it's not getting much traction :).
SQLite indeed does not support ALTER with constraints, and Hibernate does not (to my knowledge) offer a clean way to use custom SQL.
On top of this, it is not recommended to use Hibernate: hbm2ddl.auto=update in production.
For those reasons, I decided to turn myself to Flyway and write my own SQL. The good news is that adding Flyway provides my database migrations. The bad news is that it's one more dependency to maintain.
What I've done:
Added the flyway dependency in my build.gradle.kts
implementation("org.flywaydb:flyway-core:8.4.3")
Instantiated flyway before hibernate in my code, pointing to the same database:
val flyway = Flyway
.configure()
.dataSource("jdbc:sqlite:test.db", null, null).load()
flyway.migrate()
Added a handwritten SQL migration file in resources/V1__Create_person_and_entries_table.sql
create table Person
(
id integer primary key,
name varchar(255) not null UNIQUE
);
Et voilà!
I wrote a blog post over here with more details and some more reasons to use something like Flyway.
I am querying a view which joins three tables and returns the result like below:
select * from v_project_details
Project_ID Repo_Name Branch_Name
100 Repo1 Branch1
100 Repo1 Branch2
101 Repo2 Branch2
#Getter
#Setter
#Entity
#Table(name='v_project_details')
public class ProjectDetails{
#Id
#Column(name="Project_Id")
private int ProjectId
}
#Column(name="Repo_Name")
private String RepoName
}
#Column(name="Branch_Name")
private String BranchName
}
#Query(select p from v_project_details p)
List<ProjectDetails> findAll();
Results:
Project_ID Repo_Name Branch_Name
100 Repo1 Branch1
100 Repo1 Branch1 - I am expecting Branch2 here
101 Repo2 Branch2
when i query the table from spring jpa, i see three results but first row is repeated twice.
Looks like hibernate is not reinstantiating the object if #Id value is repeated more than once in the result set.
How do i force it to reinstantiate the object ? I do not have an unique identifier in the view as my view is joined from different tables.
#Id is used by JPA to identify the primary key. In your case, project_id as your primary key, which JPA understands that both row1 and row2 as same records. If project_id is not your unique identifier for your records but instead a combination of multiple fields like (eg: combination of project_id, repo_name and branch_name), you need to define a composite primary key based on those 3 fields. refer to below post by baledung to know more on composite primary keys.
Composite primary keys with JPA
JPA defines #Id as primary key field. Here is how w3scools defines primary key. Cite from https://www.w3schools.com/sql/sql_primarykey.ASP
The PRIMARY KEY constraint uniquely identifies each record in a table.
Primary keys must contain UNIQUE values, and cannot contain NULL values.
A table can have only ONE primary key; and in the table, this primary key can consist of single or multiple columns (fields).
So, primary key is always unique and prohibits null values.
JPA implementations can make use of it and store items effectively with assumption that #Id is always unique.
Your Project_ID can't be #Id since it's not unique, so you should remove this annotation and everything should work as expected. JPA requires entity to have primary key, so you can define composite key or include row number in your view, so you can make row number primary key.
In my project, an Admin (User) can set to receive scheduled emails about any User he chooses.
I need to have a database of the following design:
TABLE User (
UserId INT PRIMARY KEY AUTO_INCREMENT,
Email VARCHAR,
FirstName VARCHAR,
LastName VARCHAR
IsAdmin BOOL,
...
)
TABLE Email_Schedule (
ScheduleId INT PRIMARY KEY AUTO_INCREMENT, /* this is not necessary */
AdminId INT, /* could be replaced by a composite foreign primary keys */
UserId INT,
FOREIGN KEY (AdminId) REFERENCES User (UserId),
FOREIGN KEY (UserId) REFERENCES User (UserId)
)
The following code in my Java classes for JPA entity:
#Entity
public class Email_Schedule {
#Id
private int scheduleId;
#ManyToOne(targetEntity = User.class)
private List<User> admins = new LinkedList<>();
#ManyToOne(targetEntity = User.class)
private List<User> users = new LinkedList<>();
public Email_Schedule() {
super();
}
public Email_Schedule(User admin, User user) {
super();
this.admins.add(admin);
this.users.add(user);
}
// setters and getters...
generates a database of the following schema:
TABLE USER (
...
)
TABLE SCHEDULE (
ScheduleId INT PRIMARY KEY AUTO_INCREMENT
)
TABLE Email_Schedule (
ScheduleId INT,
Users INT,
Admins INT,
FOREIGN KEY (ScheduleId) REFERENCES SCHEDULE(ScheduleId),
FOREIGN KEY (Users) REFERENCES USER (UserId),
FOREIGN KEY (Admins) REFERENCES USER (UserId)
)
My question is why did it create a useless table for ScheduleId and referenced it from another table instead of just using it directly in Email_Schedule table?
The problem seems to be with the ScheduleId.. I tried not to use it by creating IdClass but I got different errors and wrong database designs.
EclipseLink is using TABLE for generating sequence for scheduleId.
This seems to be the default.
You can use a table for identifier generation on any database. This
strategy is completely portable across databases and will be
automatically generated for you when schema generation is enabled.
As per EclipseLink Documentation, you may have to use generation strategy of IDENTITY for scheduleId to avoid the TABLE appraoch.
#GeneratedValue(strategy=GenerationType.IDENTITY)
Note that if you use AUTO strategy as shown below, then, even in that case, EclipseLink may pick TABLE strategy for ID generation.
#GeneratedValue(strategy=GenerationType.AUTO)
Using a Default Generation Strategy
Specifying a strategy of AUTO
allows EclipseLink to select the strategy to use. Typically,
EclipseLink picks TABLE as the strategy, since it is the most portable
strategy. However, when AUTO is specified, schema generation must be
used at least once in order for the default table to be created in the
database.
More details here at PrimaryKey and GeneratedValue Documentation
I am working on a project with Hibernate and its Envers jar. I am also using Oracle 11g. It works fine except for one thing. When I give a #Id and #GeneratedValue to an #Audited entity say User and I insert new lines in my USERS table the id skips a value ( the ids should be like 3,4,5,6... but are 3,5,7,... ) and the REV column in the USERS_AUD table also skips sometimes. Probably, that's bacause the ID and REV fields in the USERS_AUD table cannot be equal ( an entity in that table cannot have the same id and revision number ). How can I modify this behavior so that the ids in my USERS table are generating normally ?
In my spring project, the tables in database are created automatically by Hibernate using my entity classes as base, but I insert some default values in the table manually (using pgAdmin3).
Because that, I am facing now this problem: when I try insert a value via Java code in one of the tables which already have values, I receive a error message, saying the primary key already exists in the database.
Anyone knows how to solve this problem?
UPDATE
That's how I declare my primary key in my class:
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
Call this SQL query once per table to set the sequence to the next free number:
SELECT setval('tblname_id_seq', max(id)) FROM tblname;
tblname being the actual name of the table.
Hibernate may use a different naming convention, or the sequence may have been renamed. If you can't find the sequence behind the serial column, check with (per documentation):
SELECT pg_get_serial_sequence(tblname, column_name)
More details:
Modify Django AutoField start value
How to import a CSV to postgresql that already has ID's assigned?
The problem here might be that you declare the id as a primitive instead of a wrapper.
So instead of:
private int id;
You should have:
private Integer id;
When you create the entity with the id is initialized as 0, instead of NULL.
That's why you get duplicate id constraint violation exceptions.
Only when the id is NULL the AUTO generation strategy will delegate the id assignment to the database.