Dynamically add new column to MySQL in Spring Boot - mysql

Say I have a class that holds user info
User.java
#Data
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
//User Data
#Column(name = "first_name", nullable = false)
private String firstName;
#Column(name = "last_name", nullable = false)
private String lastName;
}
In front end page admin can update Bio Dynamically by defining new field. Say clicking on + button he can add new field called middle name, age or address, etc.
P.S. It is kind of admin privilege and number of updates runtime will be limited and hence no issue of creating unlimited fields.
How can I address this dynamic addition of entity in MySQL using Spring Boot?

You might add new fields using a custom map, for example:
#ElementCollection(fetch = FetchType.LAZY)
#CollectionTable(name = "custom_fields")
#MapKeyColumn(name = "field")
private Map<String, String> customFields;

Related

How to properly design a table with field as list or set

I have a user that can have a wallet. Now, when user create a wallet I want to give him a option to create a transaction on that wallet. So, on that form I would like to have next fields, so prompt user to insert:
Amount of transaction, Date, note of transaction, category of transaction
So far I have this in Spring:
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "transaction_id")
private Integer id;
private double amount;
private String note;
#DateTimeFormat(pattern = "dd-MM-yyyy")
#Column(name = "date")
private Date date;
But I have problem with field category. I want to prompt user to pick from dropdown menu one of category from the list. But how to create a field categories that will be filled with names of categories?
I tried with:
#Column(name = "categories")
private List categories;
But I'm getting:
Could not determine type for: java.util.List, at table: transaction, for columns: [org.hibernate.mapping.Column(categories)]
Spring JPA lets you handle this a couple of different ways. Since you didn't specify what type of thing a category is, I am assuming you want it to be a String.
#ElementCollection
The first, easiest, and generally recommended way is to use the #ElementCollection. If your categories are fairly well fixed, you can even use enums for this.
#ElementCollection
private List<String> categories=new ArrayList<>();
With this, JPA will generate a second table in the database called transaction_categories You don't have to create an Entity for this table or anything. Everything is handled under the covers.
JsonB
If you are using postgres 10+ (I think) you can make a column into a JSON object. You will need the following dependency in your gradle.
implementation 'com.vladmihalcea:hibernate-types-52:2.15.1'
And you will need to change your model thus:
#TypeDefs({
#TypeDef(name = "json", typeClass = JsonType.class)
})
#Entity
public class Transaction {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "transaction_id")
private Integer id;
private double amount;
private String note;
#DateTimeFormat(pattern = "dd-MM-yyyy")
#Column(name = "date")
private Date date;
#Type(type = "json")
#Column(columnDefinition = "jsonb")
private List<String> categories=new ArrayList<>();
}
So long as this list does not become gigantic, it is a pretty decent solution.

Spring boot - Auto-increment problem after data insert through SQL

I am developing a spring boot project, linked to a MySQL database through Hibernate.
Everything runs fine when the database starts empty. However, when the dummy data data.sql file runs before lauching the application, and then inserting objects through the application, the insertion doesn't take into account previous IDs, therefore resulting in duplicate entries until the number of lines is reached :
I'll try explaining better with an example :
At application start, the data.sql file inserts 3 dummy users.
Reaching registration page to add a new user and submitting, Hibernate returns an error 'Duplicate entry '1' for PRIMARY key', then 'Duplicate entry '2' for PRIMARY key' and 'Duplicate entry '3' for PRIMARY key' after retrying two times.
At the fourth retry, the user is added.
The ID therefore does auto-increment, but doesn't take into account previously inserted rows through data.sql.
Note that I've tried playing around with the generation type (AUTO / IDENTITY) of the User class with no success, and my hibernate datasource is on create-drop mode.
Update :
User entity :
#Data
#Builder
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Table(name = "users")
public class User {
public final static Role DEFAULT_ROLE = new Role();
static {
DEFAULT_ROLE.setId(2);
DEFAULT_ROLE.setRole("NORMAL");
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id")
private int id;
#Column(name = "user_name")
#Length(min = 5, message = "*Your user name must have at least 5 characters")
#NotEmpty(message = "*Please provide a user name")
private String userName;
#Column(name = "email")
#Email(message = "*Please provide a valid Email")
#NotEmpty(message = "*Please provide an email")
private String email;
#Column(name = "password")
#Length(min = 5, message = "*Your password must have at least 5 characters")
#NotEmpty(message = "*Please provide your password")
private String password;
#Column(name = "name")
#NotEmpty(message = "*Please provide your name")
private String name;
#Column(name = "last_name")
#NotEmpty(message = "*Please provide your last name")
private String lastName;
#Column(name = "active")
private Boolean active;
#ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
#JoinTable(name = "user_role", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
#Column(name = "phone_number")
#NotEmpty(message = "*Please provide a phone number")
private String phoneNumber;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "address_id", referencedColumnName = "address_id")
private Address address;
}
Any idea what would solve it? :)
Cheers.
The problem is not in Hibernate, but in the statements in data.sql.
In those INSERT statements your probably give explicit values for the user_id column, hence MySQL does not increment its AUTO_INCREMENT counter. Hibernate's default generation strategy for MySQL is IDENTITY, which relies on the AUTO_INCREMENT feature.
The easiest solution is to omit the users_id column in the INSERT statements. You can however also set the initial AUTO_INCREMENT value for the table to a higher number:
ALTER TABLE users AUTO_INCREMENT=1001
Edit: Your identity field is declared as int id, therefore it can never be null. Change it to Integer, so that Hibernate can distinguish between persisted (id != null) and not persisted entities.

Create and persist properly in the MySQL database

I work with a Spring boot application and this is the entity class I work,
#Entity
public class User {
// form:hidden - hidden value
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// form:input - textbox
#Column(name = "name", columnDefinition = "VARCHAR(30)", nullable = false)
String name;
// form:input - textbox
#Column(name = "email", columnDefinition = "VARCHAR(50)", nullable = false)
String email;
// form:input - password
#Column(name = "password", columnDefinition = "VARCHAR(20)", nullable = false)
String password;
// form:textarea - textarea
#Column(name = "address", columnDefinition = "VARCHAR(255)")
String address;
// form:input - password
String confirmPassword;
// form:checkbox - single checkbox
#Column(name = "newsletter")
boolean newsletter;
// form:radiobutton - radio button
#Column(name = "sex", columnDefinition = "VARCHAR(1)")
String sex;
// form:radiobuttons - radio button
#Column(name = "number")
Integer number;
// form:select - form:option - dropdown - single select
#Column(name = "country", columnDefinition = "VARCHAR(10)")
String country;
// form:checkboxes - multiple checkboxes
#ElementCollection
#NotNull
List<String> framework;
// form:select - multiple=true - dropdown - multiple select
#ElementCollection
List<String> skill;
}
I intend to only create the following columns in the user table,
id,
name,
email,
address,
password,
newsletter,
framework,
sex,
Number,
Country,
Skill
For example, the corresponding SQL will be like (though I don't want to write explicitly),
CREATE TABLE user (
id LONG GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) PRIMARY KEY,
name VARCHAR(30),
email VARCHAR(50),
address VARCHAR(255),
password VARCHAR(20),
newsletter BOOLEAN,
framework VARCHAR(500),
sex VARCHAR(1),
Number INTEGER,
Country VARCHAR(10),
Skill VARCHAR(500)
);
The starting controller class,
#Controller
public class UserController {
private final Logger logger = LoggerFactory.getLogger(UserController.class);
#Autowired
private UserService userService;
private static List<User> populateDefaultUserValues() {
List<User> users = new ArrayList<>();
User user = new User();
user.setName("Ella");
user.setEmail("xyz#gmail.com");
user.setPassword("df32d343H");
user.setFramework(Arrays.asList("Spring MVC, GWT".split("\\s*,\\s*")));
users.add(user);
user = new User();
user.setName("Alex");
user.setEmail("alex#hotmail.com");
user.setPassword("12HH2d343H");
user.setFramework(Arrays.asList("Spring MVC, GWT".split("\\s*,\\s*")));
users.add(user);
user = new User();
user.setName("Romanna");
user.setEmail("romanna#hotmail.com");
user.setPassword("Rommann343");
user.setFramework(Arrays.asList("Spring MVC, GWT".split("\\s*,\\s*")));
users.add(user);
return users;
}
#GetMapping(value = "/")
public String index() {
return "redirect:/users";
}
#GetMapping(value = "/users")
public String showAllUsers(Model model) {
List<User> users = populateDefaultUserValues();
users.forEach(user -> {
userService.save(user);
});
model.addAttribute("users", userService.findAll());
return "list";
}
}
When I run the app, I see that this created in the MYSQL database,
How do I omit the confirmed_password field and add the columns of skill and framework? I expect them to be VARCHAR?
How do I omit the confirmed_password field
You should use the #Transient annotation.
add the columns of skill and framework?
Since you've marked them as #ElementCollection - there will be no columns in user table, as you can't persist a collection of values in a single relation attribute (it would violate 1NF restrictions). There are two additional tables, named like user_framework and user_skill, they are related to user table with user_id columns.
You may read the Java Persistense wiki and the Hibernate documentation about collections to find different examples of proper usage. Although, your code is fine and, in common case, you should not worry about database tables, relations and data consistency - ORM framework (Hibernate, by default) will handle all these things for you.

Could not write content: failed to lazily initialize a collection of role

I have One-To-Many relationship, here is my code
#Entity
#Table(name = "catalog")
public class Catalog {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "catalog_id")
private int catalog_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#OneToMany(mappedBy="mycatalogorder")
private List<Order> orders;
#OneToMany(mappedBy="mycatalog")
private List<CatalogItem> items;
// setters and getters
}
#Entity
#Table(name = "catalogitem")
public class CatalogItem {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "catalogitem_id")
private int catalogitem_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#NotEmpty
#Column(name = "price", nullable = false)
private Double price;
#OneToOne(mappedBy="ordercatalogitem", cascade=CascadeType.ALL)
private OrderItem morderitem;
#ManyToOne
#JoinColumn(name="catalog_id", nullable=false)
private Catalog mycatalog;
// setters and getters
}
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "order_id")
private int order_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#NotEmpty
#Size(min = 3, max = 1024)
#Column(name = "note", nullable = false)
private String note;
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(pattern = "ddmmYYYY HH:mm:ss")
#Column(name = "created", nullable = false)
private Date created;
#OneToMany(mappedBy="myorder")
private Set<OrderItem> orderItems;
#ManyToOne
#JoinColumn(name="catalog_id", nullable=false)
private Catalog mycatalogorder;
#PrePersist
protected void onCreate() {
created = new Date();
}
// setters and getters
}
#Entity
#Table(name = "orderitem")
public class OrderItem {
#Id
#Column(name="catalogitem_id", unique=true, nullable=false)
#GeneratedValue(generator="gen")
#GenericGenerator(name="gen", strategy="foreign", parameters=#Parameter(name="property", value="catalogitem"))
private int catalogitem_id;
#Column(name = "quantity")
private int quantity;
#OneToOne
#PrimaryKeyJoinColumn
private CatalogItem ordercatalogitem;
#ManyToOne
#JoinColumn(name="order_id", nullable=false)
private Order myorder;
// setters and getters
}
And I am getting the exception:
org.springframework.http.converter.HttpMessageNotWritableException:
Could not write content: failed to lazily initialize a collection of
role: com.example.helios.model.Catalog.items, could not initialize
proxy - no Session; nested exception is
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily
initialize a collection of role:
com.example.helios.model.Catalog.items, could not initialize proxy -
no Session
org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:271)
org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:100)
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:222)
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:183)
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:80)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:126)
My versions is:
SpringFramework 4.2.4.RELEASE
Hibernate 4.3.11.Final
Jackson 2.7.4
Jacksontype 2.7.1
This is the normal Hibernate behaviour
In one to many relations, hibernate loads the father entity (Catalog in your case) but it will load the children entities List (List items and List orders in your case) in a LAZY mode
This means you can't access to these objects because they are just proxies and not real objects
This is usefull in order to avoid to load the full DB when you execute a query
You have 2 solution:
Load children entities in EAGER mode (I strongly suggest to you to not do it because you can load the full DB.... but it is something related to your scenario
You don't serialize in your JSON the children entities by using the com.fasterxml.jackson.annotation.JsonIgnore property
Angelo
A third option which can be useful if you don't want to use EAGER mode and load up everything is to use Hibernate::initialize and only load what you need.
Session session = sessionFactory.openSession();
Catalog catalog = (Catalog) session.load(Catalog.class, catalogId);
Hibernate.initialize(shelf);
More information
I had the same problem but a fixed by:
#OneToMany
#JoinColumn(name = "assigned_ingredient", referencedColumnName = "ingredient_id")
#Fetch(FetchMode.JOIN) // Changing the fetch profile you can solve the problem
#Where(clause = "active_ind = 'Y'")
#OrderBy(clause = "meal_id ASC")
private List<Well> ingredients;
you can have more information here: https://vladmihalcea.com/the-best-way-to-handle-the-lazyinitializationexception/
It's caused by an infinite loop when parsing datas to JSON.
You can solve this by using #JsonManagedReference and #JsonBackReference annotations.
Definitions from API :
JsonManagedReference (https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonManagedReference.html) :
Annotation used to indicate that annotated property is part of two-way
linkage between fields; and that its role is "parent" (or "forward")
link. Value type (class) of property must have a single compatible
property annotated with JsonBackReference. Linkage is handled such
that the property annotated with this annotation is handled normally
(serialized normally, no special handling for deserialization); it is
the matching back reference that requires special handling
JsonBackReference: (https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonBackReference.html):
Annotation used to indicate that associated property is part of
two-way linkage between fields; and that its role is "child" (or
"back") link. Value type of the property must be a bean: it can not be
a Collection, Map, Array or enumeration. Linkage is handled such that
the property annotated with this annotation is not serialized; and
during deserialization, its value is set to instance that has the
"managed" (forward) link.
Example:
Owner.java:
#JsonManagedReference
#OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
Set<Car> cars;
Car.java:
#JsonBackReference
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "owner_id")
private Owner owner;
Another solution is to use #JsonIgnore which will just set null to the field.
Here is my solution for this task with Hibernate. I marked hibernate releation with #JsonIgnore and use custom field for jackson, in which I check if the field is loaded. If you need serialize collection to json then you should manualy call collection getter during hibernate transaciton.
#JsonIgnore
#OneToMany(mappedBy = "myorder")
private List<OrderItem> orderItems = new ArrayList<>();
#JsonProperty(value = "order_items", access = JsonProperty.Access.READ_ONLY)
private List<OrderItem> getOrderItemsList() {
if(Hibernate.isInitialized(this.relatedDictionary)){
return this.relatedDictionary;
} else{
return new ArrayList<>();
}
}
#JsonProperty(value = "order_items", access = JsonProperty.Access.WRITE_ONLY)
private void setOrderItemsList(List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
I know this is an old post but this might still help someone facing a similar issue. To solve the problem, iterate through the list of items and set the lazy-loadable collection to null. Then set your mapper to include NON-NULL
for (Catalog c : allCatalogs) {
c.setItems(null);
}
objectMapper.setSerializationInclusion(Include.NON_NULL)
Using FetchType.LAZY , if still getting the error "Could not write content: failed to lazily initialize a collection of role" , that may be probably caused by somewhere in the logic (perhaps in a controller) , Catalog is being tried to be deserialized that contains list of catalog items which is a proxy but the transaction has already ended to get that.
So create a new model ('CatalogResource' similar to catalog but without the list of items).
Then create a catalogResource object out of the Catalog (which is returned from the query)
public class CatalogResource {
private int catalog_id;
private String name;
private List<Order> orders;
}
I think the best solution to your problem (which also is the simplest) is to set your FetchType to LAZY and simply annotate the oneToMany collection fields using #transient.
Setting FetchType to EAGER isn't a good idea most times.
Best of luck.
"You don't serialize in your JSON the children entities by using the com.fasterxml.jackson.annotation.JsonIgnore property"
Add #JsonIgnore for hibernate lazy loading properties eg. #ManyToOne. That should work

Spring Data doesn't seem to understand #Column name

I'm using Spring-boot 1.4.1.RELEASE, with Spring Data and Hibernate to persist some data into a MySQL database.
I have this class, Respondent, annotated with #Entity and one of the fields annotated as below:
#Column(name = "firstName", nullable = false, length = 100)
private String firstName;
When I try to save a Respondent to the DB by calling save() on its CrudRepository<Respondent, Long>, I get this error:
ERRORcom.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'first_name' in 'field list'
This error had started occurring before I had the #Column annotation for the field, so I thought it was some default Hibernate behaviour to map firstName to first_name, but I've added the #Column annotation to it and nothing changed. Is it still wrong? I've already rebuilt the application with mvn clean package.
Here's my Respondent entity:
#Entity
public class Respondent {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column(name = "firstName", nullable = false, length = 100)
private String firstName;
private String tussenvoegsel;
#Column(name = "lastName", nullable = false, length = 100)
private String lastName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Company_id", nullable = false)
private Company company;
By default Spring uses jpa.SpringNamingStrategy to generate the table names.
The ImprovedNamingStrategy will convert CamelCase to SNAKE_CASE where as the EJB3NamingStrategy just uses the table name unchanged.
You can try to change the naming_strategy to:
spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.EJB3NamingStrategy
or the #Column name attribute should be in lowercase #Column(name = "firstname")
For Hibernate 5 this should work (I am not quite sure if you also need the above one. But try it with both):
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
There we need to follow some naming strategy.
The column name should be in lowercase or uppercase.
#Column(name="FIRSTNAME")
private String firstName;
or
#Column(name="firstname")
private String firstName;
keep in mind that, if you have your column name "first_name" format in the database then you have to follow the following way
#Column(name="firstName")
private String testName;