I'm looking for a solution to check a duplicate key before commit my BeanFieldGroup with JPAContainer. There's some a solution for this without using CriteriaQuery ?
For example, when I execute the commit there's some way to check if the key already exists in database and returns a exception ?
I'm trying this
#Entity
#Table(name="curriculum")
public class Curriculum implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
private Long idCurriculum;
#Email
#NotEmpty
#NotNull
#Size(max=250)
#Column(unique=true)
private String email;
}
/** BeanFieldGroup */
private final BeanFieldGroup<Curriculum> binder = new BeanFieldGroup<Curriculum>(Curriculum.class);
private final Curriculum bean = new Curriculum();
/** datasources */
private final CustomJPAContainer<Curriculum> datasource = new CustomJPAContainer<Curriculum>(Curriculum.class);
private VerticalLayout buildLayout(){
vLayout = new VerticalLayout();
vLayout.setMargin(true);
vLayout.setSpacing(true);
binder.setItemDataSource(bean);
Field<?> field = null;
//email unique key on database
field = binder.buildAndBind("Email", "email", TextLower.class);
email = (TextLower)binder.getField("email");
email.setWidth("350px");
email.setMaxLength(250);
vLayout.addComponent(email);
return vLayout;
}
#Override
public void buttonClick(ClickEvent event) {
if((event.getButton()) == btnSalvar){
try {
binder.commit();
} catch (CommitException e) {
Notification.show(e.getMessage(),
Type.ERROR_MESSAGE);
}
try {
datasource.addEntity(binder.getItemDataSource().getBean());
} catch(Exception e) {
Notification.show(e.getMessage(),
Type.ERROR_MESSAGE);
return;
}
}
After make commit I do: datasource.addEntity(binder.getItemDataSource().getBean()); if datasource.addEntity() doesn't work and does execute a generic Exception.
How do I check if email field already exist in database before commit without using CriteriaQuery, there are a way ?
Any idea ?
How about this (per Svetlin Zarev's suggestion):
binder.addCommitHandler(new CommitHandler()
{
#Override
public void preCommit(CommitEvent commitEvent) throws CommitException
{
String email = (String) commitEvent.getFieldBinder().getField("email").getValue();
EntityManager em = /* retrieve your EntityManager */;
List<?> results = em.createQuery("select idCurriculum from Curriculum where email = ?")
.setParameter(1, email)
.getResultList();
if (results.size() > 0)
throw new CommitException("Email already exists");
}
#Override
public void postCommit(CommitEvent commitEvent) throws CommitException { }
});
Change your DB to have unique indexes. It will throw an exception when you try to insert duplicate data.
Related
I'm creating an app using the mvvm pattern with android room, but I've ran into some trouble validating user input. When a user wants to add an ingredient to the app, they are required to enter a name for this ingredient. I want the app to notify the user if the name is already in use. I have tried some stuff using the Transformations.Map() functions but without any success.
I'm fairly new to the mvvm pattern and LiveData, and I've been stuck on this for quite a while now so any advice would be appreciated.
This is the ingredient entity:
#Entity(tableName = "ingredient")
public class BaseIngredient {
#PrimaryKey(autoGenerate = true)
private int id;
private String name;
private String category;
#ColumnInfo(name = "cooking_time")
private int cookingTime;
#Ignore
public BaseIngredient() {
}
public BaseIngredient(int id, #NonNull String name, #NonNull String category, int cookingTime)
throws InvalidValueException {
this.id = id;
setName(name);
setCookingTime(cookingTime);
setCategory(category);
}
public void setName(String name) throws InvalidNameException {
if (name == null || name.isEmpty())
throw new InvalidNameException("Name is empty");
if (!name.matches("[A-z0-9]+( [A-z0-9]+)*"))
throw new InvalidNameException("Name contains invalid tokens");
this.name = name;
}
public void setCategory(String category) throws InvalidCategoryException {
if (category == null || category.isEmpty())
throw new InvalidCategoryException("Category is empty");
if (!category.matches("[A-z0-9]+"))
throw new InvalidCategoryException("Category contains invalid tokens");
this.category = category;
}
public void setCookingTime(int cookingTime) throws InvalidCookingTimeException {
if (cookingTime < 1)
throw new InvalidCookingTimeException("Time must be positive");
this.cookingTime = cookingTime;
}
/* getters */
public boolean isValid() {
return name != null && category != null && cookingTime != 0;
}
This is the IngredientRepository I'm using:
private IngredientDao ingredientDao;
private LiveData<List<BaseIngredient>> ingredients;
public IngredientRepository(Application application) {
LmcfyDatabase database = LmcfyDatabase.getDatabase(application.getApplicationContext());
ingredientDao = database.ingredientDao();
ingredients = ingredientDao.getAllIngredients();
}
public LiveData<List<BaseIngredient>> getAllIngredients() {
return ingredients;
}
public LiveData<List<BaseIngredient>> getIngredientsWithQuery(String query) {
return ingredientDao.getIngredientsWithQuery("%" + query + "%");
}
public void insert(BaseIngredient ingredient) {
LmcfyDatabase.databaseWriteExecutor.execute(() -> {
ingredientDao.insert(ingredient);
});
}
public LiveData<Integer> getIngredientsWithNameCount(String name) {
return ingredientDao.getIngredientsWithNameCount(name);
}
The IngredientDao:
#Insert(onConflict = OnConflictStrategy.IGNORE, entity = BaseIngredient.class)
long insert(BaseIngredient ingredient);
#Delete(entity = BaseIngredient.class)
void delete(BaseIngredient ingredient);
#Query("SELECT * FROM ingredient")
LiveData<List<BaseIngredient>> getAllIngredients();
#Query("SELECT * FROM ingredient WHERE name LIKE :query")
LiveData<List<BaseIngredient>> getIngredientsWithQuery(String query);
#Query("SELECT COUNT(id) FROM ingredient WHERE name LIKE :name")
LiveData<Integer> getIngredientsWithNameCount(String name);
And finally the ViewModel that is used to create an Ingredient
private final IngredientRepository repository;
private final BaseIngredient ingredient;
private final MutableLiveData<String> nameError;
private final MutableLiveData<String> categoryError;
private final MutableLiveData<String> cookingTimeError;
private final MutableLiveData<Boolean> ingredientValidStatus;
public AddIngredientViewModel(#NonNull Application application) {
super(application);
repository = new IngredientRepository(application);
ingredient = new BaseIngredient();
nameError = new MutableLiveData<>();
categoryError = new MutableLiveData<>();
cookingTimeError = new MutableLiveData<>();
ingredientValidStatus = new MutableLiveData<>();
}
public void onNameEntered(String name) {
try {
ingredient.setName(name);
nameError.setValue(null);
} catch (InvalidNameException e) {
nameError.setValue(e.getMessage());
} finally {
updateIngredientValid();
}
}
public void onCategoryEntered(String category) {
try {
ingredient.setCategory(category);
categoryError.setValue(null);
} catch (InvalidCategoryException e) {
categoryError.setValue(e.getMessage());
} finally {
updateIngredientValid();
}
}
public void onCookingTimeEntered(int cookingTime) {
try {
ingredient.setCookingTime(cookingTime);
cookingTimeError.setValue(null);
} catch (InvalidCookingTimeException e) {
cookingTimeError.setValue(e.getMessage());
} finally {
updateIngredientValid();
}
}
private void updateIngredientValid() {
ingredientValidStatus.setValue(ingredient.isValid());
}
public boolean saveIngredient() {
if (ingredient.isValid()) {
Log.d(getClass().getName(), "saveIngredient: Ingredient is valid");
repository.insert(ingredient);
return true;
} else {
Log.d(getClass().getName(), "saveIngredient: Ingredient is invalid");
return false;
}
}
The onXXEntered() functions in the viewmodel are linked to the textViews in the fragment, and the saveIngredient() function is called when a save button is pressed. The XXError LiveData's are used to display errors to the user.
The real problem lies in the fact that LiveData is async, and the user can change their input and click the save button before the LiveData contains the result from the database. If I want the check the input upon saving it, the 'add activity' will have already finished before the check is done.
Again, any help would be very much appreciated.
I had to do something similar in one of my recent projects. What I did was:
Room cannot create columns with SQLite Unique constraint (if it is not the PrimaryKey - which is your case). So don't initialize the database in your app code using Room. Instead, create a database file outside your application. Add a Unique constraint on the 'name' column. Then add the database file in your project under assets folder. (for example, create a subfolder inside assets - 'db_files' - and copy your pre-created database file under this folder)
I guess you use Singleton pattern for your #DataBase class. Replace your 'getInstance()' method with following:
public static MyDB getInstance(final Context context) {
if(INSTANCE == null) {
synchronized (AVListDB.class) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
MyDB.class,"myDB.db")
.createFromAsset( "db/myDB.db")
.build();
}
}
return INSTANCE;
}
This creates a copy of your pre-packaged database file under applications database files path.
With unique constraint in place, your #Insert and #Update annotated methods will respect the constraint condition, and will throw a SQLiteConstraintException if you try to insert a previously used name. You can catch this exception, and pass it to your View or ViewModel as you wish (I implemented a simple centralized event publisher component).
I hope that helps.
I want to use custom id generator in hibernate. This is my model:
#Entity(name="Poli")
#Table(name="POLI")
public class Poli extends DefaultEntityImpl implements Serializable{
#Id
#GenericGenerator(
name = "string-sequence",
strategy = "id.rekam.medis.service.generator.IdGenerator",
parameters = {
#org.hibernate.annotations.Parameter(
name = "sequence_name",
value = "pol_seq"),
#org.hibernate.annotations.Parameter(
name = "sequence_prefix",
value = "POL-")
})
#GeneratedValue(
generator = "string-sequence",
strategy = GenerationType.SEQUENCE)
#Basic(optional = false)
#Column(name = "ID",nullable = false)
private String id;
#Column(name = "NAMA", length = 10)
private String nama;
//getter setter
}
And my IdGenerator Class is :
public class IdGenerator implements IdentifierGenerator, Configurable {
private static final Log logger = LogFactory.getLog(IdGenerator.class);
private String sequenceName;
private String sequencePrefix;
public static final String SEQUENCE_PREFIX = "sequence_prefix";
#Override
public Serializable generate(SessionImplementor session, Object obj) throws HibernateException {
Connection con = session.connection();
Long nextValue = null;
try {
PreparedStatement p = con.prepareStatement(" SELECT POL_SEQ.NEXTVAL FROM DUAL ");
ResultSet rs = p.executeQuery();
while(rs.next()) {
nextValue = rs.getLong("nextVal");
}
} catch (SQLException e) {
e.printStackTrace();
}
if(logger.isDebugEnabled()) logger.debug("new id is generated:" + nextValue);
return "POL-" + nextValue;
}
#Override
public void configure(Type type, Properties params, Dialect dlct) throws MappingException {
sequencePrefix = ConfigurationHelper.getString(SEQUENCE_PREFIX, params,"SEQ_");
}
}
My Goal is, I want that my IdGenerator Class can be used for all Entities/Models. Just need to change the paramters in entity.
My Question: How to catch the parameters in the IdGenerator Class?
I want to get "pol_seq" and "POL-" in IdGenerator Class.
Hot Regard,
Tarmizi
That's what you've implemented the Configurable Interface for.
The configure() Method has these parameters in the Properties parameter. Look at its JavaDoc, it's basically a HashMap, so just do
params.getProperty("sequence_prefix");
And maybe you want to turn these names into constants, either public static final Strings, or better yet Enums.
I have this repository:
public interface AccountsRepository extends CrudRepository<Account, Long> {
}
Then I have this service class:
public class AccountService {
private AccountsRepository accountsRepository;
public AccountService(AccountsRepository accountsRepository) {
this.accountsRepository = accountsRepository;
}
public Account createNewAccount() {
Account account = new Account();
account = tryStoreAccountElseRepeatWithDifferentIdentifier(account);
return account;
}
private Account tryStoreAccountElseRepeatWithDifferentIdentifier(Account account) {
account.setIdentifier(IdentifierGenerator.generateString(6));
try {
return accountsRepository.save(account);
} catch (DataIntegrityViolationException e) {
return tryStoreAccountElseRepeatWithDifferentIdentifier(account);
}
}
}
Unit test:
public class AccountServiceUnitTests {
AccountService fixture;
AccountsRepository mockAccountRespository;
#Before
public void setup() {
mockAccountRespository = mock(AccountsRepository.class);
fixture = new AccountService(mockAccountRespository);
}
#Test
public void repeatCreateAccountWhenIdentifierIsDuplicateValue() {
Account account = new Account();
account.setId(123L);
account.setIdentifier("ABCDEF");
when(mockAccountRespository.save(any(Account.class)))
.thenThrow(DataIntegrityViolationException.class)
.thenThrow(DataIntegrityViolationException.class)
.thenThrow(DataIntegrityViolationException.class)
.thenReturn(account);
Account newAccount = fixture.createNewAccount();
assertEquals(account, newAccount);
}
}
Entity:
#Entity
#Table(name = "accounts")
public class Account {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "identifier", unique = true)
private String identifier;
#Column(name = "username", unique = true)
private String username;
// getter, setter shortened
}
Thing is, I want to store a new Account into the database. Some columns have index UNIQUE. So when you try to insert data MySQL throws an exception if there is a DUPLICATE value.
Since the save(Entitiy) method does not throw a documented exception I tried what would happen and saw that a DataIntegrityViolationException is thrown in case I try to add duplicate value.
So my idea was basically try recursively to insert a new row till no exception is thrown. See: tryStoreAccountElseRepeatWithDifferentIdentifier
Is this way of checking for duplicate value good practise? Or is there a "built-in" solution I don't know?
ElasticSearch makes index for new records created by UI,but the records created by liquibase file not indexed so it don't appears in search result,ElasticSearch should index all records created by UI and liquibase files,Is there any process for indexing the records in liquibase files.
Liquibase only makes changes to your database. Unless you have some process which listens to the database changes and then updates Elasticsearch, you will not see the changes.
There might be multiple ways to get your database records into Elasticsearch:
Your UI probably calls some back-end code to index a create or an update into Elasticsearch already
Have a batch process which knows which records are changed (e.g. use an updated flag column or a updated_timestamp column) and then index those into Elasticsearch.
The second option can either be done in code using a scripting or back-end scheduled job or you might be able to use Logstash with the jdbc-input plugin.
As Sarwar Bhuiyan and Mogsdad sad
Unless you have some process which listens to the database changes and
then updates Elasticsearch
You can use liquibase to populate elasticsearch(this task will be executed once, just like normal migration). To do this you need to create a customChange:
<customChange class="org.test.ElasticMigrationByEntityName">
<param name="entityName" value="org.test.TestEntity" />
</customChange>
In that java based migration you can call the services you need. Here is an example of what you can do (please do not use code from this example in a production).
public class ElasticMigrationByEntityName implements CustomTaskChange {
private String entityName;
public String getEntityName() {
return entityName;
}
public void setEntityName(String entityName) {
this.entityName = entityName;
}
#Override
public void execute(Database database) {
//We schedule the task for the next execution. We are waiting for the context to start and we get access to the beans
DelayedTaskExecutor.add(new DelayedTask(entityName));
}
#Override
public String getConfirmationMessage() {
return "OK";
}
#Override
public void setUp() throws SetupException {
}
#Override
public void setFileOpener(ResourceAccessor resourceAccessor) {
}
#Override
public ValidationErrors validate(Database database) {
return new ValidationErrors();
}
/* ===================== */
public static class DelayedTask implements Consumer<ApplicationContext> {
private final String entityName;
public DelayedTask(String entityName) {
this.entityName = entityName;
}
#Override
public void accept(ApplicationContext applicationContext) {
try {
checkedAccept(applicationContext);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//We're going to find beans by name (the most controversial point)
private void checkedAccept(ApplicationContext context) throws ClassNotFoundException {
Class entityClass = Class.forName(entityName);
String name = entityClass.getSimpleName();
//Please do not use this code in production
String repositoryName = org.apache.commons.lang3.StringUtils.uncapitalize(name + "Repository");
String repositorySearchName = org.apache.commons.lang3.StringUtils.uncapitalize(name + "SearchRepository");
JpaRepository repository = (JpaRepository) context.getBean(repositoryName);
ElasticsearchRepository searchRepository = (ElasticsearchRepository) context.getBean(repositorySearchName);
//Doing our work
updateData(repository, searchRepository);
}
//Write your logic here
private void updateData(JpaRepository repository, ElasticsearchRepository searchRepository) {
searchRepository.saveAll(repository.findAll());
}
}
}
Because the beans have not yet been created, we will have to wait for them
#Component
public class DelayedTaskExecutor {
#Autowired
private ApplicationContext context;
#EventListener
//We are waiting for the app to launch
public void onAppReady(ApplicationReadyEvent event) {
Queue<Consumer<ApplicationContext>> localQueue = getQueue();
if(localQueue.size() > 0) {
for (Consumer<ApplicationContext> consumer = localQueue.poll(); consumer != null; consumer = localQueue.poll()) {
consumer.accept(context);
}
}
}
public static void add(Consumer<ApplicationContext> consumer) {
getQueue().add(consumer);
}
public static Queue<Consumer<ApplicationContext>> getQueue() {
return Holder.QUEUE;
}
private static class Holder {
private static final Queue<Consumer<ApplicationContext>> QUEUE = new ConcurrentLinkedQueue();
}
}
An entity example:
#Entity
#Table(name = "test_entity")
#Document(indexName = "testentity")
public class TestEntity implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Field(type = FieldType.Keyword)
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
#NotNull
#Column(name = "code", nullable = false, unique = true)
private String code;
...
}
I am trying to insert a record in the database (using Java EE 6, EJB 3.1, JPA 2.0). I am getting an error that accountTypeId field is null, but i have set it up as autogenerate. Can anyone please suggest what am I doing wrong?
Following is the create table query:
create table example.account_type(
account_type_id INT NOT null PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),
account_type_desc varchar(20)
);
Following is the entity class:
EDIT: Updated the entity class as generated by NetBeans which didn't work. I also added #GeneratedValue annotation but still it didn't work.
#Entity
#Table(name = "ACCOUNT_TYPE")
#NamedQueries({
#NamedQuery(name = "AccountType.findAll", query = "SELECT a FROM AccountType a"),
#NamedQuery(name = "AccountType.findByAccountTypeId", query = "SELECT a FROM AccountType a WHERE a.accountTypeId = :accountTypeId"),
#NamedQuery(name = "AccountType.findByAccountTypeDesc", query = "SELECT a FROM AccountType a WHERE a.accountTypeDesc = :accountTypeDesc")})
public class AccountType implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY) // ADDED THIS LINE
#Basic(optional = false)
#NotNull
#Column(name = "ACCOUNT_TYPE_ID")
private Integer accountTypeId;
#Size(max = 50)
#Column(name = "ACCOUNT_TYPE_DESC")
private String accountTypeDesc;
public AccountType() {
}
public AccountType(Integer accountTypeId) {
this.accountTypeId = accountTypeId;
}
public Integer getAccountTypeId() {
return accountTypeId;
}
public void setAccountTypeId(Integer accountTypeId) {
this.accountTypeId = accountTypeId;
}
public String getAccountTypeDesc() {
return accountTypeDesc;
}
public void setAccountTypeDesc(String accountTypeDesc) {
this.accountTypeDesc = accountTypeDesc;
}
#Override
public int hashCode() {
int hash = 0;
hash += (accountTypeId != null ? accountTypeId.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof AccountType)) {
return false;
}
AccountType other = (AccountType) object;
if ((this.accountTypeId == null && other.accountTypeId != null) || (this.accountTypeId != null && !this.accountTypeId.equals(other.accountTypeId))) {
return false;
}
return true;
}
#Override
public String toString() {
return "Entities.AccountType[ accountTypeId=" + accountTypeId + " ]";
}
}
Following is the session bean interface:
#Remote
public interface AccountTypeSessionBeanRemote {
public void createAccountType();
public void createAccountType(String accDesc);
}
Following is the session bean implementation class:
#Stateless
public class AccountTypeSessionBean implements AccountTypeSessionBeanRemote {
#PersistenceContext(unitName="ExamplePU")
private EntityManager em;
#Override
public void createAccountType(String accDesc) {
AccountType emp = new AccountType(accDsc);
try {
this.em.persist(emp);
System.out.println("after persist");
} catch(Exception ex) {
System.out.println("ex: " + ex.getMessage());
}
}
}
Following is the Main class:
public class Main {
#EJB
private static AccountTypeSessionBeanRemote accountTypeSessionBean;
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
accountTypeSessionBean.createAccountType("test");
}
}
Following is the error:
INFO: ex: Object: Entities.AccountType[ accountTypeId=null ] is not a known entity type.
You are not getting an error because of "accountTypeId field is null". As the error message says, the error occurs because "Entities.AccountType[ accountTypeId=null ] is not a known entity type".
The most likely reason is that AccountType is not annotated with #Entity. This problem is likely solved by adding it. Additionally it makes sense to use
#GeneratedValue(strategy = GenerationType.IDENTITY)
instead of AUTO. Auto means that the provider chooses a strategy based on the capabilities of the target database. According to the table definition it seems clear that the preferred strategy is IDENTITY.
I changed my create table query as following:
create table example.account_type(
account_type_id INT NOT null PRIMARY KEY,
account_type_desc varchar(20)
);
Then had to add the following line to the entity class (Netbeans doesn't add that):
#GeneratedValue(strategy = GenerationType.AUTO)