It is interesting point that it sometimes performs insert operation correctly. I do not know why and how this situation can be happens. So, I could not figure out where I made a mistake(s).
Here is my project files.
1) SentFilesDao.java
#Dao
public interface SentFilesDao {
#Query("SELECT id, name, type, status, dateTime FROM sent")
LiveData<List<SentFilesEntity>> getAllSentFiles();
#Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(SentFilesEntity file);
#Query("DELETE FROM sent")
void deleteAllRecords();
}
2) SentFilesRepository.java
public class SentFilesRepository {
private SentFilesDao mSentFilesDao;
private LiveData<List<SentFilesEntity>> mAllSentFiles;
SentFilesRepository(Application application) {
AppDatabase db = AppDatabase.getDatabase(application);
mSentFilesDao = db.mSentFilesDao();
mAllSentFiles = mSentFilesDao.getAllSentFiles();
}
LiveData<List<SentFilesEntity>> getAllSentFiles() { return mAllSentFiles; }
public void insert(SentFilesEntity sentFilesEntity) {
new SentFilesRepository.insertAsyncTask(mSentFilesDao).execute(sentFilesEntity);
}
private static class insertAsyncTask extends AsyncTask<SentFilesEntity, Void, Void> {
private SentFilesDao mAsyncTaskDao;
insertAsyncTask(SentFilesDao dao) {
mAsyncTaskDao = dao;
}
#Override
protected Void doInBackground(final SentFilesEntity... params) {
mAsyncTaskDao.insert(params[0]);
Log.e("doInBackground: ", "reached here!"); // no any log in Logcat about this. Weird...
return null;
}
}
}
3) SentFilesEntity.java
#Entity(tableName = "sent")
public class SentFilesEntity {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
public int s_f_id;
#ColumnInfo(name = "name")
public String s_f_name;
#ColumnInfo(name = "type")
public String s_f_type;
#ColumnInfo(name = "status")
public String s_f_operation_status;
#ColumnInfo(name = "dateTime")
public String s_f_time;
}
4) SentFilesViewModel.java
public class SentFilesViewModel extends AndroidViewModel {
private SentFilesRepository mRepository;
private LiveData<List<SentFilesEntity>> mAllSentFiles;
public SentFilesViewModel(Application application) {
super(application);
mRepository = new SentFilesRepository(application);
mAllSentFiles = mRepository.getAllSentFiles();
}
public LiveData<List<SentFilesEntity>> getAllSentFiles() { return mAllSentFiles; }
public void insert(SentFilesEntity sentFile) { mRepository.insert(sentFile); }
}
What I am trying to do:
My program sends selected files from one device to another one through WiFi Direct. As file received or sent, I want to save some information about file (such as file name, file type (image, video, music, APK and etc.), send/receive date and time) and operation (succeeded or failed). I can handle all these things.
What is the problem?
Well, getting all the properties (those I mentioned above), is not end. Main problem is saving that data to database. To do that I created some required files (I shared their codes above). When file sent, I am trying to insert related information to database. But it does not insert it. That is the my problem.
Well, main codes are too long to post here. However I posted my latest codes to my GitHub project and the main logic starts from here and it connected to FileStatisticsActivity.
p.s: Maybe sharing the project link is not ethic, I am so sorry for that.
Thanks.
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'm terribly sorry if I can't start another post which is connected to my previous one but my question is somewhat different.
I noticed that I really can save new data in my database as long as I never added data to the database by using the line spring.datasource.initialization-mode=always in my application.properties and made a data.sql file with a few insert statements. Once I insert the data using that file, I can access the data and show it to the user, but I can't create any new data because I get the following error
ERROR: duplicate key value violates unique constraint "joke_pkey"
Detail: Key (id)=(1) already exists.
Does anyone know how to help me with this? I'm doing an interview task and I am meant to first import data using the data.sql file and then later add some more data.
The post with my code is here:
Spring Boot using save never inserts a row inside of a Postgresql table
EDIT - someone recommended adding my code here directly and saying what I've tried.
I have tried to initialize the database with the application properties the way they are, then restarting the app but without the last line, and setting the spring.jpa.hibernate.ddl-auto to none. But even so, it didn't work. I genuinely expected it to work like that. Because if the table is empty and I fill it in using the functions I created, everything works like a charm, even after restarting the server (id keep the ring.jpa.hibernate.ddl-auto to none again to keep the data from being deleted)
I have also tried simply changing the GenerationType.AUTO to GenerationType.TABLE strategy in my Joke class, but that didn't seem to change anything either.
application.properties :
spring.datasource.url=jdbc:postgresql://localhost:5432/flyway_demo
spring.datasource.username=bob
spring.datasource.password=bob123
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=create
spring.datasource.initialization-mode=always
My Web Controller that has the post function:
#PostMapping("/post")
public String insertJoke(JokeForm jokeForm) {
int categoryid = jokeForm.getCategoryId();
String content = jokeForm.getContent();
databasController.insert(categoryid, content);
return "redirect:/";
}
My DBController whose insert function is being called
public Joke insert(int categoryid, String content) {
return jokeRepository.save(new Joke(categoryid, content));
}
Most of my Joke data class:
#Entity
public class Joke {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(columnDefinition = "serial")
private Long id;
#NotNull
#Column(name = "category_id_FK")
private long categoryId;
#NotBlank
private String content;
#Column(columnDefinition = "integer default 0")
private int likes = 0;
#Column(columnDefinition = "integer default 0")
private int dislikes = 0;
public Joke() {
}
public Joke(long categoryid, String content) {
this.setCategoryid(categoryid);
this.setContent(content);
}
// id
public Long getId() {
return this.id;
}
// id
public void setId(Long id) {
this.id = id;
}
// categoryid
public long getCategoryid() {
return this.categoryId;
}
public void setCategoryid(long categoryid) {
this.categoryId = categoryid;
}
// content
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
// likes
public int getLikes() {
return this.likes;
}
public void setLikes(int likes) {
this.likes = likes;
}
// dislikes
public int getDislikes() {
return this.dislikes;
}
public void setDislikes(int dislikes) {
this.dislikes = dislikes;
}
}
Joke Repository:
#Repository
public interface JokeRepository extends JpaRepository<Joke, Integer> {
Joke findById(long id);
List<Joke> findByCategoryid(int categoryid);
}
It seems that all you need to do is change GenerationType.AUTO to GenerationType.IDENTITY.
Reason behind this is the sequence, which might be out of sync if you use AUTO. Because then hibernate uses its own sequence instead of the one postgres creates when using serial.
I'm trying to unit-test my DAO using android-room. I have written an insert test that works properly. Unfortunately, the delete method doesn't seem to be working.
I've tried a few different setups for the test. None have worked.
Here is the DAO:
#Dao
public interface MonthlyDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
void saveAll(List<Monthly> goals);
#Insert(onConflict = OnConflictStrategy.REPLACE)
void save(Monthly goal);
#Update
void update(Monthly goal);
#Delete
void delete(Monthly goal);
#Query("SELECT * FROM Monthly")
LiveData<List<Monthly>> findAll();
#Query("SELECT * FROM monthly")
List<Monthly> findAllList();
}
Here is the Monthly entity:
#Entity
public class Monthly {
#PrimaryKey(autoGenerate = true)
private int monthlyId;
#TypeConverters(CalendarTypeConverter.class)
#ColumnInfo(name = "date")
private Calendar date = Calendar.getInstance();
#ColumnInfo(name = "title")
private String title;
#ColumnInfo(name = "description")
private String description;
#ColumnInfo(name = "completed")
private boolean completed;
...
public int getMonthlyId() {
return monthlyId;
}
public void setMonthlyId(int monthlyId) {
this.monthlyId = monthlyId;
}
And here is the test I am running:
#RunWith(AndroidJUnit4.class)
public class MonthlyTest {
private MonthlyDao monthlyDao;
private MonthlyGoalsDatabase db;
#Before
public void createDb() {
Context context = ApplicationProvider.getApplicationContext();
db = Room.inMemoryDatabaseBuilder(context, MonthlyGoalsDatabase.class).build();
monthlyDao = db.getMonthlyDao();
}
#After
public void closeDb() throws IOException {
db.close();
}
#Test
public void deleteGoal() throws Exception {
String title = "test delete title";
Calendar date = Calendar.getInstance();
date.set(Calendar.HOUR_OF_DAY, 0);
String desc = "test delete desc";
Monthly goal = new Monthly(title, date, desc);
monthlyDao.save(goal);
List<Monthly> goals = monthlyDao.findAllList();
Assert.assertThat(goals.get(0).getTitle(), equalTo(goal.getTitle()));
monthlyDao.delete(goal);
List<Monthly> updatedGoals = monthlyDao.findAllList();
Assert.assertTrue(updatedGoals.isEmpty());
}
I except the updatedGoals list to be empty, but it isn't. There is still the goal that I inserted during the test.
The method annotated with #Delete uses the primary key on the entity to know which row to delete from the database (because there could be multiple rows with the same data but different keys).
However, you're using the initial goal object that you created, which has no primary key, and thus cannot be used to indicate which row to remove.
Try doing this:
monthlyDao.save(goal);
List<Monthly> goals = monthlyDao.findAllList();
Assert.assertThat(goals.get(0).getTitle(), equalTo(goal.getTitle()));
monthlyDao.delete(goals.get(0)); // <-- Delete the goal returned from the find, which will have an ID
List<Monthly> updatedGoals = monthlyDao.findAllList();
Assert.assertTrue(updatedGoals.isEmpty());
That could easily be cleaned up a bit, but the above example only changes one line, to make it clear where the issue is.
See here for the relevant documentation.
UPDATE:
At this point I can not create any new Realm-based objects. Each and every one fails with the same error, no matter if I clean, rebuild, etc. To test this, I created a new class "Sample":
package com.reddragon.intouch.model;
import java.util.UUID;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Sample extends RealmObject {
#PrimaryKey
private String id;
private String sampleField;
public Sample() {
this(UUID.randomUUID().toString());
}
private Sample(String id) {
this.id=id;
}
public String getId(){
return id;
}
public String getSampleField() {
return sampleField;
}
public void setSampleField(String sampleField) {
this.sampleField = sampleField;
}
}
And in my MainActivity.java, I try and create a new instance:
try {
MediaDataMgr.get().addSample(new Sample());
Timber.d("Lifecycle: was able to add Sample");
} catch (Exception e) {
Timber.d("Got exception instantiating Sample: %s", e.getMessage());
}
This addSample() method uses a similar approach to the two classes in this project that DO work:
public String addSample(Sample s) {
boolean success = true;
Sample sample;
Realm realm;
String retVal = null;
boolean mainThread = Thread.currentThread().equals(Looper.getMainLooper().getThread());
if (mainThread) {
realm = mUIThreadRealm;
} else {
realm = Realm.getDefaultInstance();
}
try {
realm.beginTransaction();
sample = realm.createObject(Sample.class,s.getId()); //<--CRASH!!!!
sample.setSampleField(s.getSampleField());
} catch (Exception e ) {
Timber.d( "Exception adding a Sample: %s", e.getMessage());
success = false;
} finally {
if ( success ) {
realm.commitTransaction();
retVal = s.getId();
} else {
realm.cancelTransaction();
}
if (!mainThread) {
realm.close();
}
}
return retVal;
}
I am completely stuck now on this project.
UPDATE:
I completely commented all references to the 'Contact' object in my app, and then deleted Contact.java from my project. I did a full Rebuild, then ran it, and everything worked.
I then created a new class Contact.java and entered the same fields etc. as before, and uncommented references to it in the rest of my project. I did a rebuild and ran - and got the same error.
I then refactored the name of the Contact class to ContactSharingInfo, thinking there may be class name clash somewhere. Rebuild and run and again - same error, this time referencing the new class name.
ORIGINAL POST:
I am using gradle plugin and annotations processor 5.9.1. I created a new class ("Contact.java"), which initially worked ok. I then adjusted the class (removed a couple of fields, added a new field), and I started receiving this error. I have tested this on a Samsung S7 Edge (API 26) as well as several emulators. Same problem.
I have done all manner of clean, rebuild, invalidate caching and restart, etc. No help. I reviewed bug #3819 and #4579, but nothing in there has helped. I have disabled instant run. No help.
The stack trace is:
realmSet$id:111, com_reddragon_intouch_model_ContactRealmProxy (io.realm)
<init>:30, Contact (com.reddragon.intouch.model)
<init>:26, Contact (com.reddragon.intouch.model)
<init>:84, com_reddragon_intouch_model_ContactRealmProxy (io.realm)
newInstance:96, DefaultRealmModuleMediator (io.realm)
createObjectInternal:1048, Realm (io.realm)
createObject:1024, Realm (io.realm)
addContact:877, MediaDataMgr (com.reddragon.intouch.model)
The code in question that causes this (addContact() in the MediaDataMgr class where I centralize Realm access) is:
public String addContact(Contact c, int status) {
boolean success = true;
Contact contact;
Realm realm;
String retVal = null;
boolean mainThread = Thread.currentThread().equals(Looper.getMainLooper().getThread());
if (mainThread) {
realm = mUIThreadRealm;
} else {
realm = Realm.getDefaultInstance();
}
try {
realm.beginTransaction();
contact = realm.createObject(Contact.class,c.getId()); // <--CRASH HAPPENS HERE
contact.setEmailAddress(c.getEmailAddress());
contact.setDisplayName(c.getDisplayName());
contact.setStatus(status);
} catch (Exception e ) {
Timber.d( "Exception adding a contact: %s", e.getMessage());
success = false;
} finally {
if ( success ) {
realm.commitTransaction();
retVal = c.getId();
} else {
realm.cancelTransaction();
}
if (!mainThread) {
realm.close();
}
}
return retVal;
}
And the Contact class referenced in the stack trace is:
public class Contact extends RealmObject implements CardListable {
#PrimaryKey
private String id;
private String displayName;
private String emailAddress;
private String pathToProfilePic; // This will always be a URI, but we have to store it as a string and convert to URI at runtime.
#Ignore
private int status = STATUS_UNKNOWN;
public Contact() {
this(UUID.randomUUID().toString());
}
private Contact(String id) {
this.id = id;
}
public String getId(){
return id;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getPathToProfilePic() {
return pathToProfilePic;
}
public void setPathToProfilePic(String pathToProfilePic) {
this.pathToProfilePic = pathToProfilePic;
}
public String getFirstLineDesc() {
return displayName;
}
public String getSecondLineDesc() {
return emailAddress;
}
}
When debugging into the com_reddragon_intouch_model_ContactRealmProxy.java class, I find that the exception occurs because the variable 'proxyState' is null when the method public String realmSet$id() is called:
public void realmSet$id(String value) {
if (proxyState.isUnderConstruction()) { //<-- CRASH HAPPENS HERE
// default value of the primary key is always ignored.
return;
}
proxyState.getRealm$realm().checkIfValid();
throw new io.realm.exceptions.RealmException("Primary key field 'id' cannot be changed after object was created.");
}
Which leads me to believe that the initialization of proxyState in realm$injectObjectContext() is not being called.
This same approach to creating a new object is working fine with two other classes in this same project, and I verified that realm$injectObjectContext() IS being called there. Here is the stack trace in the same type of construction for my Media.java class (which works):
realm$injectObjectContext:105, com_reddragon_intouch_model_MediaRealmProxy (io.realm)
<init>:52, Media (com.reddragon.intouch.model)
<init>:49, Media (com.reddragon.intouch.model)
<init>:99, com_reddragon_intouch_model_MediaRealmProxy (io.realm)
newInstance:99, DefaultRealmModuleMediator (io.realm)
createObjectInternal:1048, Realm (io.realm)
createObject:1024, Realm (io.realm)
addMedia:267, MediaDataMgr (com.reddragon.intouch.model)
So something about how the code is generated for the Contact class must be different from the other ones that work - and that "something" is confusing Realm to not call that method.
What to do about it?
SOLUTION:
In my case I was an unwitting victim of Lint refactoring. It turns out I had run some Lint processes across my classes, and one of the suggested changes was to change certain constructors to be declared private. Apparently Realm doesn't like this in the model classes that take default args in the constructor. So, for example the Contact.java class had this constructor:
public class Contact extends RealmObject implements CardListable {
#PrimaryKey
private String id;
private String displayName;
private String emailAddress;
private String pathToProfilePic; // This will always be a URI, but we have to store it as a string and convert to URI at runtime.
#Ignore
private int status = STATUS_UNKNOWN;
public Contact() {
this(UUID.randomUUID().toString());
}
private Contact(String id) { //<--NOTE PRIVATE CONSTRUCTOR
this.id = id;
}
Changing to this:
public Contact() {
this(UUID.randomUUID().toString());
}
public Contact(String id) { //<--NOTE THIS IS NOW PUBLIC
this.id = id;
}
and doing a clean+rebuild solved the problem.
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;
...
}