Given table schema as below (this is mysql syntax, but does not matter)
-- base table keeping all subscription data
CREATE TABLE user_subscription (
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INTEGER NOT NULL,
data VARCHAR(2000) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- supportive view focusing on every user's latest row
CREATE VIEW user_subscription_latest AS
SELECT s1.*
FROM user_subscription s1
LEFT JOIN user_subscription s2
ON s2.user_id = s1.user_id AND s2.updated_at > s1.updated_at
WHERE s2.id IS NULL;
And I have following entity class for base table user_subscription
import javax.persistence.*;
#Entity
#Table(name = "user_subscription")
public class UserSubscription implements java.io.Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false, updatable = false, insertable = false)
private Integer id;
#Column(name = "user_id", nullable = false)
private Integer userId;
#Column(name = "data", nullable = true, length = 2000)
private String data;
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "created_at", nullable = false, updatable = false, insertable = false)
private java.time.LocalDateTime createdAt;
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "updated_at", nullable = false, updatable = false, insertable = false)
private java.time.LocalDateTime updatedAt;
}
And following Spring Data JPA repository (still does not matter. it can be any JPA scenario)
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface UserSubscriptionRepository extends JpaRepository<UserSubscription, Integer> {
// TODO How can I achieve this without using nativeQuery?
#Query(value = "SELECT * FROM user_subscription_latest", nativeQuery = true)
java.util.List<UserSubscription> findLatest();
}
Since view user_subscription_latest is subset of user_subscription, they are interchangeable, but I have no idea how to put them together.
My question is what is the correct/preferred way to design the JPA entity so that I can take advantage of supportive view user_subscription_latest on query while keeping accessibility to base table user_subscription, without nativeQuery?
To answer your question on what is the recommended way (other than using a nativeQuery) to gain accessibility to your view as well as maintain your existing accessibility to the user table, the best possibility is to create another entity something like the one below,
import javax.persistence.*;
#Entity
#Immutable
public class UserSubscriptionLatest implements java.io.Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false, updatable = false, insertable = false)
private Integer id;
#Column(name = "user_id", nullable = false)
private Integer userId;
#Column(name = "data", nullable = true, length = 2000)
private String data;
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "created_at", nullable = false, updatable = false, insertable = false)
private java.time.LocalDateTime createdAt;
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "updated_at", nullable = false, updatable = false, insertable = false)
private java.time.LocalDateTime updatedAt;
}
Making your entity class immutable is the most important step to working with database views in Spring Data JPA. Based on this mapping, Hibernate will ensure that your application doesn’t perform any write operations on the view. To make your persistence layer easier to use, you should take this one step further and also exclude all methods from your repository that persist, update or remove an entity.
Then create a readOnlyRepository defining an interface that extends the Repository interface and copying a few method definitions from Spring Data JPA’s standard repositories like the one below
#NoRepositoryBean
public interface ReadOnlyRepository<T, ID> extends Repository<T, ID> {
List<T> findAll();
List<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
Optional<T> findById(ID id);
long count();
}
Then create your JPArepository by extending the readOnlyRepository that we just created, this ensures that your repository is accessible for readOnly purposes and also tells the hibernate to eschew any checks pertaining to write/update
public interface UserSubscriptionLatestRepository extends ReadOnlyRepository<UserSubscriptionLatest, Integer> {
List<UserSubscriptionLatest> findById();
}
Hope I had answered your question
Related
Hi hibernate (envers) experts!
I currently have two/three entities
Automations which represent an automated process for one answer of a question (of a survey)
Answers answer of a user for a question (of a survey)
BaseEntity which is extended by Automations and Answers an has basic attributes like modify timestamps and so on.
The Automations table is audited by hibernate envers.
I want to fetch all deleted Automations for one specific Answer. The generated and executed query from hibernate does include my condition for the "deleted" revtype of envers but not my condition for the answer.
See the function listHistoryByAnswer
#MappedSuperclass
public class BaseEntity extends PanacheEntityBase {
#Id
#Column(name = "UUID", updatable = false, nullable = false, columnDefinition = "uuid")
private UUID _uuid;
#Column(name = "CREATED_AT", nullable = false, updatable = false, columnDefinition = "TIMESTAMP without time zone")
#CreationTimestamp
private Timestamp createdAt;
#Column(name = "CREATED_BY", nullable = false, updatable = false)
private String createdBy;
#Column(name = "UPDATED_AT", columnDefinition = "TIMESTAMP without time zone")
#UpdateTimestamp
private Timestamp updatedAt;
#Column(name = "UPDATED_BY")
private String updatedBy;
}
#Entity
#Table(name = "AUTOMATION", uniqueConstraints = #UniqueConstraint(columnNames = {"ANSWER_UUID"}))
#Audited
public class Automation extends BaseEntity {
#Basic
#Column(name = "DESCRIPTION")
private String description;
#JsonBackReference("automation-answer")
#ManyToOne
#JoinColumn(name = "ANSWER_UUID", nullable = false)
private Answer answer;
}
#Entity
#Table(name = "ANSWER")
#Audited(targetAuditMode = NOT_AUDITED)
public class Answer extends BaseEntity {
#JsonManagedReference("automation-answer")
#OneToMany(mappedBy = "answer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Automation> automations = new ArrayList<>();
#Column(name = "DATA", columnDefinition = "jsonb")
private JsonNode data;
}
/**
* List old (deleted) questions based on the answer
* #param answerUuid unique id of the answer
* #return list with deleted questions
*/
public List<Automation> listHistoryByAnswer(final UUID answerUuid){
List resultList = AuditReaderFactory.get(getEntityManager())
.createQuery()
.forRevisionsOfEntity(Automation.class, true)
.add(AuditEntity.revisionType().eq(RevisionType.DEL))
.add(AuditEntity.property("answer_uuid").eq(answerUuid))
.getResultList();
return resultList;
}
Generated SQL
select automation0_.UUID as uuid1_1_0_,
automation0_.REV as rev2_1_0_,
defaultrev1_.REV as rev1_15_1_,
automation0_.REVTYPE as revtype3_1_0_,
automation0_.ANSWER_UUID as answer_u9_1_0_,
defaultrev1_.REVTSTMP as revtstmp2_15_1_
from A_AUTOMATION_HIS automation0_
cross join
REVINFO defaultrev1_
where automation0_.REVTYPE = 2 -- DELETED entities
and automation0_.REV = defaultrev1_.REV
order by automation0_.REV asc;
These are the entities defined in my project:
Post
#Entity
public class Post implements Serializable {
public enum Type {TEXT, IMG}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Integer id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "section_id")
protected Section section;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "author_id")
protected User author;
#Column(length = 255, nullable = false)
protected String title;
#Column(columnDefinition = "TEXT", nullable = false)
protected String content;
#Enumerated(EnumType.STRING)
#Column(nullable = false)
protected Type type;
#CreationTimestamp
#Column(nullable = false, updatable = false, insertable = false)
protected Instant creationDate;
#Column(insertable = false, updatable = false)
protected Integer votes;
/*accessor methods*/
}
Comment
#Entity
public class Comment implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Integer id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "post_id")
protected Post post;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "author_id")
protected User author;
#ManyToOne(fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "parent_comment_id")
protected Comment parentComment;
#Column(columnDefinition = "TEXT", nullable = false)
protected String content;
#CreationTimestamp
#Column(nullable = false, updatable = false, insertable = false)
protected Instant creationDate;
#Column(insertable = false, updatable = false)
protected Integer votes;
#Column(insertable = false, updatable = false)
protected String path;
/*accessor methods*/
}
As you can see, aggregations are only tracked from the child's point of view: there are no #ManyToOne relationships due to a design choice.
However, I'd like to add a commentCount field to the Post entity. This field should be loaded eagerly (since it is always read in my use cases and a COUNT statement isn't that costly... i guess)
These are the options I'm currently aware of:
Use the #Formula or the #PostLoad annotations: there would be N+1select statements
Retrieve the posts, then call another repository method that uses the IN operator with all the retrieved posts as argument:
select post, count(comment) from Post post, Comment comment where comment.post in (:posts) group by comment.post
(Given that my data access layer might load even 50 posts at once, would a 50 parameters long in be optimal?)
Create a non-mapped entity attribute, then take care of filling it manually in all repository methods involving Post with a join: i'd like to avoid this.
Store commentCount in the database and mantain it with triggers: could be an option, but it's not the focus of this question (I'm not allowed to touch the schema)
What can be a valid solution? I'm using Hibernate but i prefer implementation-agnostic solutions (Implementation-specific options won't be ignored, though)
Using #Formula would not cause any N+1 select problem (different to #PostLoad), since the specified expression is evaluated as part of the initial select query.
(Infact, you would need to enable Hibernates byte code enhancement to be able to lazy-load the calculated #Formula property: Controlling lazy/eager loading of #Formula columns dynamically )
So even though it is a Hibernate-specific feature which needs native SQL, #Formula is probably a good and simple way to realize this.
In case the performance drawback of the correlated subquery gets significant and there are cases when the calculated property is not needed, you might additionally create a DTO projection only containing needed properties.
I think this is a bit weird, I am not sure if this is even possible, but my requirement is as follows:
I have two entities: Dataset and Organization. The Dataset entity has a many-to-one mapping to the Organization entity. Organizations are of two types: Customer and Partner. The business requirement is that only Partner type organizations can have datasets. So, is there any way to map the Dataset entity to Organization such that all foreign keys in Dataset only contain ids of Organization entities that are of type Partner?
The organization entity is defined as follows:
#Entity
#Table(name = "organization")
public class Organization {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String type;
private String name;
private String address;
private String status;
private String subtype;
#Column(name = "created_date")
#Temporal(value = TemporalType.TIMESTAMP)
private Date createdDate;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "organization_id", insertable = false, updatable = false)
private List<User> users;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns({ #JoinColumn(name = "subtype", referencedColumnName = "name", insertable = false, updatable = false),
#JoinColumn(name = "type", referencedColumnName = "organization_type", insertable = false, updatable = false) })
private OrganizationSubType organizationSubType;
// Getters and setters
.
.
.
Here, the type column will contain either PARTNER or CUSTOMER.
And here's the Dataset entity I am currently designing:
#Entity
#Table(name="datasets")
public class Dataset {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name="partner_id", nullable = false)
private long partnerId;
#Column(nullable = false, unique = true)
private String name;
private String description;
#Column(nullable = false)
#Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private DatasetStatus datasetStatus;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="partner_id", referencedColumnName="id", insertable = false, updatable = false)
private Organization partner;
//Getters and setters
So, is there any way to set a constraint on the mapping in such a way that when a new Dataset entity is persisted, the organization id always belongs to an entity of type partner and not customer? Or do I have to separate the customer and partner organizations as separate entities?
There are several option how you can achieve it:
By adding validation at business layer in the place when you build new Dataset object prior to persisting it. Such validation would check if given Organisation can be associated with created Dataset entity based on organisation type.
By utilizing Bean Validation API & defining custom constraint validator. Such validator will be invoked by each change on the entity.
Add hibernate-validator to the classpath
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.3.4.Final</version>
</dependency>
Define annotation for new validation
#Constraint(validatedBy = PartnerOrganisationValidator.class)
#Target(FIELD)
#Retention(RUNTIME)
#Documented
public #interface PartnerOrganisation {
String message() default "Organisation should be of type PARTNER";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Define contraint validator
public class PartnerOrganisationValidator implements ContraintValidator<PartnerOrganisation, Organisation> {
#Override
public boolean isValid(Organisation organisation, ConstraintValidatorContext constraintValidatorContext) {
return organisation == null || "PARTNER".equals(organisation.type);
}
}
Register validator in hibernate validator by adding fully qualified name of your validator to META-INF/services/javax.validation.ConstraintValidator file.
Last step is to use validator in your entity:
#PartnerOrganisation
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="partner_id", referencedColumnName="id", insertable = false, updatable = false)
private Organization partner;
By using SQL CHECK CONSTRAINT if database you uses supports it - just like in the example in https://stackoverflow.com/a/55379219/14231619
By modeling PARTNER Organisation and CUSTOMER Organisation as separate entity classes.
Since structure of Organisation for PARTNER and CUSTOMER is the same approach from option 4. seems overcomplicated for the situation. I would recommend going with option 1.
Scenario:
I have a products table with these fields: id, code, description, photo.
I have a product_stock view from other schema with fields: prod_code, stok_tot
Misson: What I need to do in Spring Boot is to join the products with their total stock.
Rules:
I can't use #Query on this task, I need to join them at the Product Entity Class, so that stock became some kind of Transient field of product.
I can't change the fact that product's ID is a Long and ProductStock's ID is a String, but I could use product's code field instead right? (how?)
So far... I tryed to use #OneToOne and #JoinColumn to do the job, but my REST gives me the stock field as NULL.
"Estoque.java"
#Entity
#Table(name = "VW_ESTOQUE", schema = "ASICAT")
public class Estoque {
#Id
#Column(name = "CD_BEM_SERVICO", unique = true, nullable = false)
private String codigo;
#Column(name = "ESTOQUE")
private Long estoque;
// getters and setters hidden by me
}
"Produto.java"
#Entity
#NamedEntityGraph(name = "Produto.detail", attributeNodes = #NamedAttributeNode("categorias"))
public class Produto implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE)
private Long id;
private String codigo;
private String descricao;
// here is where I get the null values
#Transient
#OneToOne(fetch = FetchType.LAZY)
#JoinTable(name = "VW_ESTOQUE", joinColumns = #JoinColumn(name = "CODIGO", referencedColumnName = "CODIGO"), inverseJoinColumns = #JoinColumn(name = "CD_BEM_SERVICO", referencedColumnName = "CODIGO"))
private Estoque estoque;
private String hash;
#ManyToMany(mappedBy = "produtos", fetch = FetchType.EAGER)
#BatchSize(size = 10)
private List<Categoria> categorias = new ArrayList<>();
// getters and setters hidden by me
}
In my product repository I call FindAll()
You have annotated Produto.estoque as #Transient, which means that it is not part of the persistent state of the entity. Such a field will be neither written nor read when instances of that entity are managed. That's not going to serve your purpose.
There are two things I can imagine you might have been trying to achieve:
That every time an Estoque is accessed via a Produto, it should be loaded from the DB to ensure its freshness. JPA does not provide for that, though you might want to annotate Estoque with #Cacheable(value = false), and specify the lazy fetch strategy on the Produto side of the relationship.
You want to avoid the persistence provider attempting to persist any changes to an Estoque, since it is backed by a view, not an updatable table. This we can address.
My first suggestion would be to map ASICAT.VW_ESTOQUE as a secondary table instead of an entirely separate entity. That might look something like this:
#Entity
#SecondaryTable(name = "VW_ESTOQUE", schema = "ASICAT"
pkJoinColumns = {
#PrimaryKeyJoinColumn(name = "CD_BEM_SERVICO",
referencedColumnName = "CODIGO") })
public class Produto implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE)
private Long id;
private String codigo;
private String descricao;
#Column(name = "ESTOQUE", table = "VW_ESTOQUE", nullable = true,
insertable = false, updatable = false)
private Long estoque;
// ...
}
You might furthermore avoid providing a setter for the estoque property.
But the SecondaryTable approach might not work well if you cannot rely on the ESTOQUE view always to provide a row for every row of PRODUTO, as there will very likely be an inner join involved in retrievals. Moreover, you don't get lazy fetches this way. The main alternative is more or less what you present in your question: to set up a separate Estoque entity.
If you do set up a separate Estoque, however, then I would approach it a bit differently. Specifically,
I would make the relationship bidirectional, so that I could
make the Estoque entity the relationship owner.
Something like this, then:
#Entity
public class Produto implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE)
private Long id;
private String codigo;
private String descricao;
// must not be #Transient:
#OneToOne(fetch = FetchType.LAZY, mappedBy = "produto", cascade = {
CascadeType.REFRESH
})
private Estoque estoque;
// ...
}
#Entity
#Table(name = "VW_ESTOQUE", schema = "ASICAT")
#Cacheable(value = false)
public class Estoque {
#Id
#Column(name = "CD_BEM_SERVICO", nullable = false,
insertable = false, updatable = false)
private String codigo;
#Column(name = "ESTOQUE", insertable = false, updatable = false)
private Long estoque;
#OneToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "CD_BEM_SERVICO", referencedColumnName = "CODIGO",
nullable = false, insertable = false, updatable = false, unique = true)
Produto produto;
// getters and setters hidden by me
}
In this case, I would avoid providing setter methods for any of the properties of Estoque, and avoid providing any constructor that allows initial property values to be set. Thus, to a first approximation, the only
way an instance's properties will take non-null values is if they are set by the persistence provider.
Additionally, since you mention Oracle, if you are using TopLink as your persistence provider then you might consider applying its #ReadOnly extension attribute to the Estoque entity, in place of or even in addition to some of these protections against trying to insert into or update the view.
I have two entities, the first of which is:
#Entity
#Table(name="E_CMS_CFG")
public class E_CMS_CFG extends BaseEntity{
#Id
#OneToOne(cascade=CascadeType.ALL)//, fetch = FetchType.EAGER)
#JoinColumns({
#JoinColumn(name = "CFG_TYPE", nullable = false, referencedColumnName = "CFG_TYPE", columnDefinition = "VARCHAR(32)"),
#JoinColumn(name = "CFG_TYPE_ID", nullable = false, referencedColumnName = "ID")
})
private E_CMS_CFG_TYPE cfgType;
The second entity is:
#Entity
#Table(name="E_CMS_CFG_TYPE")
public class E_CMS_CFG_TYPE extends BaseEntity{
#Id
#Column(name = "CFG_TYPE", length = 32)
private String cfgType;
In the first entity, I use columnDefinition = "VARCHAR(32)" on the CFG_TYPE column; however, in the database, it is creating a cfg_type integer column. Is it possible to create a VARCHAR cfg_type column and not an integer? And if it is, how? I am using Sqlite 3.8.10.1 version.
UPDATE:
The link did not help, but if I replace in E_CMS_CFG the #Id with #NaturalId (mutable = false), it works, but it creates it without a primary key.
You could try to use the natural id annotation here. For example:
#NaturalId (mutable = false)
#Column(name = "CFG_TYPE", unique = true, nullable = false, length = 32)
private String cfgType;
But I think you would still have to provide an id property and that would still be an integer value/column in your database.
I think this Question is a duplicate of "how to use id with string type in jpa hibernate"
I hope that helps!