Why instrumented room test get error FOREIGN KEY constraint failed - java

I don't understand this error, I have two entities Task and Project, one task is assign to one project (see code below).
But when I want test to add a task, I have this error:
android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
I try to add onDelete = CASCADE to FOREIGNKEY but my test does not work :/
Task Class
#Entity(tableName = "task_table",
foreignKeys = #ForeignKey(entity = Project.class,
parentColumns = "id",
childColumns = "projectId"))
public class Task {
/**
* The unique identifier of the task
*/
#PrimaryKey(autoGenerate = true)
public long id;
private long projectId;
#SuppressWarnings("NullableProblems")
#NonNull
private String name.
private long creationTimestamp;
public Task(long projectId, #NonNull String name, long creationTimestamp) {
this.setProjectId(projectId);
this.setName(name);
this.setCreationTimestamp(creationTimestamp);
}
...
TaskDao
#Dao
public interface TaskDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(Task task);
#Query("DELETE FROM task_table WHERE id = :id")
void delete(long id);
#Query("SELECT * FROM task_table ORDER BY id ASC")
LiveData<List<Task>> getAllTasks();
#Query("SELECT * FROM task_table WHERE id = :id")
LiveData<Task> getTaskById(long id);
}
Project Class
#Entity(tableName = "project_table")
public class Project {
/**
* The unique identifier of the project
*/
#PrimaryKey(autoGenerate = true)
public long id;
#NonNull
private String name;
#ColorInt
private int color;
public Project(#NonNull String name, #ColorInt int color) {
this.name = name;
this.color = color;
}
ProjectDao
#Dao
public interface ProjectDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(Project project);
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<Project> projects);
#Query("SELECT * FROM project_table")
LiveData<List<Project>> getAllProjects();
#Query("SELECT * FROM project_table WHERE id = :id")
LiveData<Project> getProjectById(long id);
}
TestInstrumented:
#Before
public void createDb() {
this.database = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(),
AppDatabase.class)
.allowMainThreadQueries()
.build();
taskDao = database.taskDao();
projectDao = database.projectDao();
}
#After
public void closeDb() {
database.close();
}
#Test
public void addTask() {
Project project = new Project("toto", 0xFFB4CDBA);
this.projectDao.insert(project);
long projectId = project.getId();
assertThat(projectId, equalTo(project.getId()));
Task task = new Task(projectId, "tâche 1", new Date().getTime());
this.taskDao.insert(task);
}
If someone can help it'll be very kind, I don't know how I can resolve this.
Thanks very much for your help

The project id will be 0 as it hasn't been set according to the inserted value when you use long projectId = project.getId();.
Thus the ForeignKey conflict when inserting the Task as the id column of the project will have been generated by SQLite and WILL not be 0 (it will be 1 or greater).
Change
#Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(Project project);
to (to get the actual generated id)
#Insert(onConflict = OnConflictStrategy.IGNORE)
long insert(Project project); // will return the id that has been generated
and then use :-
Project project = new Project("toto", 0xFFB4CDBA);
long projectId = this.projectDao.insert(project); //<<<<< returns the actual id
project.setId(projectId); // sets project id according to the inserted id BUT only if inserted and not ignored when returned value will be -1
// You should really check for -1 and handle accordingly
assertThat(projectId, equalTo(project.getId())); // not really of any use
Task task = new Task(projectId, "tâche 1", new Date().getTime());
this.taskDao.insert(task);
Note, the above is in-principle code, it has not been compiled or run/tested. It may therefore contain some errors.

Related

Primary and foreign key implementation Java and Android Room

This is my first time asking a question here in StackOverflow so forgive me if I am not asking the question right or a certain way.
I have two child classes (PerformanceAssessment and ObjectiveAssessment) from parent class Assessment. I was able to successfully do downcasting for the polymorphism part requirement of my school project, and I was able to save to their appropriate databases both instances of parent and child after downcasting (I have an assessment_table, performance_assessments, and objective_assessments table). However, I am now having Android Room database issues. I realized that I need to implement foreign keys to do the CRUD operations properly. How do I tweak my parent and child classes code and add proper annotations for implementing foreign and primary keys correctly? I need to have an autogenerated primary key for each class: parent (Assessment) and both children (PerformanceAssessment and ObjectiveAssessment). But I also need to utilize the children's primary key as a foreign key for the parent's databases so that when I delete an instance of Performance/Objective assessments, I can also delete the Assessment instance when I am downcasting. I found very little helpful information regarding this. Thanks in advance.
I am getting these errors right now:
Build Errors
Parent Class: Assessment.java
#Entity(tableName = "assessment_table")
public class Assessment {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "assessment_id")
private final int assessment_id;
private String assessmentName;
private String assessmentStart;
private String assessmentEnd;
private int courseID;
public Assessment(int assessment_id, String assessmentName, String assessmentStart, String assessmentEnd, int courseID) {
this.assessment_id = assessment_id;
this.assessmentName = assessmentName;
this.assessmentStart = assessmentStart;
this.assessmentEnd = assessmentEnd;
this.courseID = courseID;
}
Child class: PerformanceAssessment.java
#Entity(tableName = "performance_assessment", foreignKeys = {
#ForeignKey(
entity = Assessment.class,
parentColumns = "assessment_id",
childColumns = "performance_id",
onUpdate = CASCADE,
onDelete = CASCADE
)
})
public class PerformanceAssessment extends Assessment{
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "performance_id")
private int performanceID;
private String type;
public PerformanceAssessment(int assessment_id, String assessmentName, String assessmentStart, String assessmentEnd, int courseID, int performanceID, String type) {
super(assessment_id, assessmentName, assessmentStart, assessmentEnd, courseID);
this.performanceID = performanceID;
this.type = type;
}
Child class: ObjectiveAssessment.java
#Entity(tableName = "objective_assessment", foreignKeys = {
#ForeignKey(
entity = Assessment.class,
parentColumns = "assessment_id",
childColumns = "objective_id",
onUpdate = CASCADE,
onDelete = CASCADE
)
})
public class ObjectiveAssessment extends Assessment{
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "objective_id")
private int objective_ID;
private String type;
public ObjectiveAssessment(int assessmentID, String assessmentName, String assessmentStart, String assessmentEnd, int courseID, int objective_ID, String type) {
super(assessmentID, assessmentName, assessmentStart, assessmentEnd, courseID);
this.objective_ID = objective_ID;
this.type = type;
}
Here is the part where I am adding a new assessment in the app:
public void saveAssessment(View view) {
assessmentTitle = editName.getText().toString();
assessmentStart = editStart.getText().toString();
assessmentEnd = editEnd.getText().toString();
//Check if fields are empty:
if (assessmentTitle.isEmpty() || assessmentStart.isEmpty() || assessmentEnd.isEmpty()) {
Toast.makeText(AddAssessmentScreen.this, "Fill out required fields.", Toast.LENGTH_LONG).show();
return;
}
else {
//Check assessment type selected (used Downcasting for polymorphism):
if (assessment_type == true) {
Assessment performanceAssessment = new PerformanceAssessment(0, assessmentTitle, assessmentStart, assessmentEnd, currentCourseID, 0, selectedString);
PerformanceAssessment castedPerformance = (PerformanceAssessment) performanceAssessment;
//Insert assessment to database performance_assessment table (Performance child type):
repository.insert(castedPerformance);
Repository addToAssessment = new Repository(getApplication());
//Insert assessment to database assessment_table (Assessment parent type):
addToAssessment.insert(performanceAssessment);
}
else {
Assessment objectiveAssessment = new ObjectiveAssessment(0,assessmentTitle, assessmentStart, assessmentEnd, currentCourseID, 0, selectedString);
ObjectiveAssessment castedObjective = (ObjectiveAssessment) objectiveAssessment;
//Insert assessment to database objective_assessment table (Objective child type):
repository.insert(castedObjective);
Repository addToAssessment = new Repository(getApplication());
//Insert assessment to database assessment_table (Assessment parent type):
addToAssessment.insert(objectiveAssessment);
}
Toast.makeText(AddAssessmentScreen.this, "New assessment added. Refresh previous screen.", Toast.LENGTH_LONG).show();
}
How do I tweak my parent and child classes code and add proper annotations for implementing foreign and primary keys correctly?
Let the assessment_id always be the primary key i.e. don't code it in sub classes and then have a field for the reference/map/association with the parent in the subclasses.
So PerformanceAssessment could be :-
#Entity(tableName = "performance_assessment", foreignKeys = {
#ForeignKey(
entity = Assessment.class,
parentColumns = "assessment_id",
childColumns = "assessment_reference",
onUpdate = CASCADE,
onDelete = CASCADE
)
})
public class PerformanceAssessment extends Assessment {
private int assessment_reference; //<<<<<<<<<
private String type;
public PerformanceAssessment(int assessment_id, String assessmentName, String assessmentStart, String assessmentEnd, int courseID, int assessment_reference, String type) {
super(assessment_id, assessmentName, assessmentStart, assessmentEnd, courseID);
this.assessment_reference = assessment_reference;
this.type = type;
}
....
and all compiles and the underlying tables will be build using :-
_db.execSQL("CREATE TABLE IF NOT EXISTS `assessment_table` (`assessment_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assessmentName` TEXT, `assessmentStart` TEXT, `assessmentEnd` TEXT, `courseID` INTEGER NOT NULL)");
_db.execSQL("CREATE TABLE IF NOT EXISTS `performance_assessment` (`assessment_reference` INTEGER NOT NULL, `type` TEXT, `assessment_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `assessmentName` TEXT, `assessmentStart` TEXT, `assessmentEnd` TEXT, `courseID` INTEGER NOT NULL, FOREIGN KEY(`assessment_reference`) REFERENCES `assessment_table`(`assessment_id`) ON UPDATE CASCADE ON DELETE CASCADE )");
Obviously similar for the ObjectiveAssessment.
But I also need to utilize the children's primary key as a foreign key for the parent's databases so that when I delete an instance of Performance/Objective assessments, I can also delete the Assessment instance when I am downcasting.
What if, which is possible with the 1 (Assessment) - Many (PerformanceAssessments), an Assessment has multiple PerformanceAssesments? Deleting 1 PerformanceAsessment would delete the parent Assessment, which would then, due to the use of onDelete CASCADE cascade the deletions to all the other PerformanceAssessments and ObjectiveAssessments. Deletions are not propagated up to the parent (that's why the term CASCADE is used as it implies downward rather than anyway).
As an example you have an assessment with 4 PerformanceAssessments and lets say 3 ObjectiveAssessments one of the PerformanceAssesments was wrongly added. Should everything have to be deleted and re-entered to correct the 1 wrong PeformanceAssessment.
You could introduce a Trigger to automate this upwards propogation, to delete anything and delete all, if that is what you want.
Additional Re comments
I have a one to one relationship for the Assessment and Performance/Objective assessment. One assessment can only have either performance or objective type.
That may be what you wish BUT there is nothing stopping an Assessment having multiple Performance and or Objectives. That may or may not be an issue.
Also, instead of downcasting, I did upcasting instead on where I save the assessment in the app. I ran into Foreign Key Constraint Failed (code 787) and realized it was because I was downcasting. My only dilemma now is that whenever I am saving an assessment, it will only save on assessment_table and not on performance_assessment or objective_assessment tbl.
What you have to do is insert the Assessment (parent) and then use the parent's id to insert the related Peformance/Objective with the assessment_reference set the the value of the assessment.
The 787 is it saying that you cannot insert a child (Performance/Objective) without the assessment_reference being an assessment_id in the Assessment.
What you can do, which would very likely suit your situation is a) have an #Insert long insert(Assessment assessment); and an #Insert long (PerformanceAssessment performanceAssessment) (likewise for Objective)
and then use (assuming dao is an instance of the #Dao)
and use
dao.insert(new PerformanceAssessment(0,"the title", etc,dao.insert(Assessment(....),"the type");
So the Assessment is added as part of inserting the performance/Objective and with the id of the assessment being used as the assessment_reference value.
NOTE the #Insert will only return a long, so you have to convert this to an int. I would suggest changing id's and references to be long's. The overhead, if any, will have a minimal impact. Storage in the database will not be affected and it will save the hassle of converting long to int.
Demonstration
The following is a demonstration based upon code that closely reflects your but with some subtle changes.
id's have been changed to be long rather than int.
autogenerate has been removed BUT the id's will be generated as before.
2. All that autogenerate does from an SQLite standpoint is include the AUTOINCREMENT key word. This is actually inefficient and NOT recommended. https://sqlite.org/autoinc.html
So the code is :-
Assessment
#Entity(tableName = "assessment_table")
public class Assessment {
#PrimaryKey /* no need for autogenerate */
#ColumnInfo(name = "assessment_id")
/* As not autogenerate then use Long and default to null */
/* same as autogenerate but without the overheads/inefficiencies */
private Long assessment_id = null; //Long rather than long so can be null and have id generated.
private String assessmentName;
private String assessmentStart;
private String assessmentEnd;
private long courseID;
public Assessment(long assessment_id, String assessmentName, String assessmentStart, String assessmentEnd, long courseID) {
this.assessment_id = assessment_id;
this.assessmentName = assessmentName;
this.assessmentStart = assessmentStart;
this.assessmentEnd = assessmentEnd;
this.courseID = courseID;
}
#Ignore
/* Alternative constructor no need to provide id for inserting */
public Assessment(String assessmentName, String assessmentStart, String assessmentEnd, long courseID) {
this.assessmentName = assessmentName;
this.assessmentStart = assessmentStart;
this.assessmentEnd = assessmentEnd;
this.courseID = courseID;
}
public Long getAssessment_id() {
return assessment_id;
}
public void setAssessment_id(Long assessment_id) {
this.assessment_id = assessment_id;
}
public String getAssessmentName() {
return assessmentName;
}
public void setAssessmentName(String assessmentName) {
this.assessmentName = assessmentName;
}
public String getAssessmentStart() {
return assessmentStart;
}
public void setAssessmentStart(String assessmentStart) {
this.assessmentStart = assessmentStart;
}
public String getAssessmentEnd() {
return assessmentEnd;
}
public void setAssessmentEnd(String assessmentEnd) {
this.assessmentEnd = assessmentEnd;
}
public long getCourseID() {
return courseID;
}
public void setCourseID(long courseID) {
this.courseID = courseID;
}
}
PerformanceAssessment
#Entity(tableName = "performance_assessment", foreignKeys = {
#ForeignKey(
entity = Assessment.class,
parentColumns = "assessment_id",
childColumns = "assessment_reference",
onUpdate = CASCADE,
onDelete = CASCADE
)
})
public class PerformanceAssessment extends Assessment {
private long assessment_reference;
private String type;
public PerformanceAssessment(long assessment_id, String assessmentName, String assessmentStart, String assessmentEnd, long courseID, long assessment_reference, String type) {
super(assessment_id, assessmentName, assessmentStart, assessmentEnd, courseID);
this.assessment_reference = assessment_reference;
this.type = type;
}
#Ignore
public PerformanceAssessment(String assessmentName, String assessmentStart, String assessmentEnd, long courseID, long assessment_reference, String type) {
super(assessmentName, assessmentStart, assessmentEnd, courseID);
this.assessment_reference = assessment_reference;
this.type = type;
}
#Override
public Long getAssessment_id() {
return super.getAssessment_id();
}
#Override
public void setAssessment_id(Long assessment_id) {
super.setAssessment_id(assessment_id);
}
#Override
public String getAssessmentName() {
return super.getAssessmentName();
}
#Override
public void setAssessmentName(String assessmentName) {
super.setAssessmentName(assessmentName);
}
#Override
public String getAssessmentStart() {
return super.getAssessmentStart();
}
#Override
public void setAssessmentStart(String assessmentStart) {
super.setAssessmentStart(assessmentStart);
}
#Override
public String getAssessmentEnd() {
return super.getAssessmentEnd();
}
#Override
public void setAssessmentEnd(String assessmentEnd) {
super.setAssessmentEnd(assessmentEnd);
}
#Override
public long getCourseID() {
return super.getCourseID();
}
#Override
public void setCourseID(long courseID) {
super.setCourseID(courseID);
}
public long getAssessment_reference() {
return assessment_reference;
}
public void setAssessment_reference(long assessment_reference) {
this.assessment_reference = assessment_reference;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
ObjectiveAssessment
#Entity(tableName = "objective_assessment", foreignKeys = {
#ForeignKey(
entity = Assessment.class,
parentColumns = "assessment_id",
childColumns = "assessment_reference",
onUpdate = CASCADE,
onDelete = CASCADE
)
})
public class ObjectiveAssessment extends Assessment {
private long assessment_reference;
private String type;
public ObjectiveAssessment(long assessment_id, String assessmentName, String assessmentStart, String assessmentEnd, long courseID, long assessment_reference, String type) {
super(assessment_id, assessmentName, assessmentStart, assessmentEnd, courseID);
this.assessment_reference = assessment_reference;
this.type = type;
}
#Ignore
public ObjectiveAssessment(String assessmentName, String assessmentStart, String assessmentEnd, long courseID, long assessment_reference, String type) {
super(assessmentName, assessmentStart, assessmentEnd, courseID);
this.assessment_reference = assessment_reference;
this.type = type;
}
#Override
public Long getAssessment_id() {
return super.getAssessment_id();
}
#Override
public void setAssessment_id(Long assessment_id) {
super.setAssessment_id(assessment_id);
}
#Override
public String getAssessmentName() {
return super.getAssessmentName();
}
#Override
public void setAssessmentName(String assessmentName) {
super.setAssessmentName(assessmentName);
}
#Override
public String getAssessmentStart() {
return super.getAssessmentStart();
}
#Override
public void setAssessmentStart(String assessmentStart) {
super.setAssessmentStart(assessmentStart);
}
#Override
public String getAssessmentEnd() {
return super.getAssessmentEnd();
}
#Override
public void setAssessmentEnd(String assessmentEnd) {
super.setAssessmentEnd(assessmentEnd);
}
#Override
public long getCourseID() {
return super.getCourseID();
}
#Override
public void setCourseID(long courseID) {
super.setCourseID(courseID);
}
public long getAssessment_reference() {
return assessment_reference;
}
public void setAssessment_reference(long assessment_reference) {
this.assessment_reference = assessment_reference;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
A POJO has the Assessment Embedded and the Perf and Obj included via #Relation (not the ideal as if the rule of only 1 or the other than 1 will be null). So :-
AssessmentWithPerformanceAssessmentAndObjectiveAssessment
class AssessmentWithPerformanceAssessmentAndObjectiveAssessment {
#Embedded
Assessment assessment;
#Relation(
entity = PerformanceAssessment.class,
parentColumn = "assessment_id",
entityColumn = "assessment_reference"
)
PerformanceAssessment performanceAssessment;
#Relation(
entity = ObjectiveAssessment.class,
parentColumn = "assessment_id",
entityColumn = "assessment_reference"
)
ObjectiveAssessment objectiveAssessment;
}
A single #Dao annotated class :-
AssessmentDao
#Dao
abstract class AssessmentDao {
#Insert
abstract long insert(Assessment assessment);
#Insert
abstract long insert(PerformanceAssessment performanceAssessment);
#Insert
abstract long insert(ObjectiveAssessment objectiveAssessment);
/*
use to check if an Assessment has any children
*/
#Query("WITH cte_counts(counter) AS (" +
"SELECT count(*) > 0 FROM performance_assessment WHERE assessment_reference=:assessment_id " +
"UNION ALL SELECT count(*) FROM objective_assessment WHERE assessment_reference=:assessment_id" +
")" +
"SELECT sum(counter) > 0 FROM cte_counts")
abstract boolean hasChildAlready(long assessment_id);
#Transaction
#Query("SELECT * FROM assessment_table WHERE assessment_id=:assessment_id")
abstract AssessmentWithPerformanceAssessmentAndObjectiveAssessment getAssessmentWithPerformanceAssessmentOrObjectiveAssessmentById(long assessment_id);
#Transaction
#Query("SELECT * FROM assessment_table")
abstract List<AssessmentWithPerformanceAssessmentAndObjectiveAssessment> getAllAssessmentsWithPerformanceAssessmentOrObjectiveAssessmentById();
}
note an abstract class rather than an interface (could be an interface though)
An #Database annotated class
AssessmentDatabase
#Database(entities = {Assessment.class,PerformanceAssessment.class,ObjectiveAssessment.class},version =1)
abstract class AssessmentDatabase extends RoomDatabase {
abstract AssessmentDao getAssessmentDao();
private static volatile AssessmentDatabase instance = null;
static AssessmentDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context,AssessmentDatabase.class,"assessment.db")
.allowMainThreadQueries()
.build();
}
return instance;
}
}
note for convenience and brevity allows running on the main thread.
Finally actually using the above in an Activity:-
public class MainActivity extends AppCompatActivity {
AssessmentDatabase db;
AssessmentDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = AssessmentDatabase.getInstance(this);
dao = db.getAssessmentDao();
/* Various Inserts */
long loneAssessment = dao.insert(
new Assessment(
"Lone Assessment with no children",
"2021-01-15",
"2021-03-15",
0L
)
);
/* id will be 100, subsequent id's if generated will be 101,102......*/
long a1 = dao.insert(new Assessment(100,"A1","2022-01-01","2022,03-01",10));
long p1 = dao.insert(new PerformanceAssessment("P1","2022-01-31","2022-01-31",10,a1,"X"));
/* back to using generated Assessment id */
long a2 = dao.insert(new Assessment("A1","2022-01-01","2022-03-01",11));
long p2 = dao.insert(new ObjectiveAssessment("O1","2022-01-31","2022-01-31",11,a2,"Y"));
/* Build the Assessment and ObjectiveAssessment ready for insert SEE WARNING */
Assessment a10 = new Assessment("A10","2022-01-01","2022-03-01",20);
ObjectiveAssessment o10 = new ObjectiveAssessment("O10","2022-01-31","2022-01-31",a10.getCourseID(),0,"Z");
/* WARNING assessment_reference WILL NOT BE ANY GOOD (will be 0) */
long a10id = dao.insert(a10);
o10.setAssessment_reference(a10id); /*<<<<<<<<<< NOW assessment_reference should be good */
dao.insert(o10);
/* Both together */
dao.insert(
new PerformanceAssessment("P20","2022-05-07","2022-05-07",11,
/* get the Assessment ID from the insert of the Assessment */
dao.insert(
new Assessment("A20","2022-04-01","2022-06-17",21)
),
"ZZ"
)
);
/* Extract all the Assessments with their children (if any)*/
for(AssessmentWithPerformanceAssessmentAndObjectiveAssessment awpoo: dao.getAllAssessmentsWithPerformanceAssessmentOrObjectiveAssessmentById()) {
Log.d("DBINFO","Assessment is " + awpoo.assessment.getAssessmentName() + " id is " + awpoo.assessment.getAssessment_id() + " etc....");
/* need to check if the the performanceassessment is null - it will be if there isn't one */
if (awpoo.performanceAssessment != null) {
Log.d("DBINFO","\t\tPerformanceAssessment = " + awpoo.performanceAssessment.getAssessmentName() +
" id is "+ awpoo.performanceAssessment.getAssessment_id() +
" references Asessment with id " + awpoo.performanceAssessment.getAssessment_reference());
} else {
Log.d("DBINFO","\t\tNo PerformanceAssessment.");
}
/* null check see performance assessment check above */
if (awpoo.objectiveAssessment != null) {
Log.d("DBINFO","\t\tObjectiveAssessment = " + awpoo.objectiveAssessment.getAssessmentName() +
" id is "+ awpoo.objectiveAssessment.getAssessment_id() +
" references Asessment with id " + awpoo.objectiveAssessment.getAssessment_reference());
} else {
Log.d("DBINFO","\t\tNo ObjectiveAssessment.");
}
}
}
}
When run (first time, will fail if run twice due to use of 100 for the id) the log shows :-
D/DBINFO: Assessment is Lone Assessment with no children id is 1 etc....
D/DBINFO: No PerformanceAssessment.
D/DBINFO: No ObjectiveAssessment.
D/DBINFO: Assessment is A1 id is 100 etc....
D/DBINFO: PerformanceAssessment = P1 id is 1 references Asessment with id 100
D/DBINFO: No ObjectiveAssessment.
D/DBINFO: Assessment is A1 id is 101 etc....
D/DBINFO: No PerformanceAssessment.
D/DBINFO: ObjectiveAssessment = O1 id is 1 references Asessment with id 101
D/DBINFO: Assessment is A10 id is 102 etc....
D/DBINFO: No PerformanceAssessment.
D/DBINFO: ObjectiveAssessment = O10 id is 2 references Asessment with id 102
D/DBINFO: Assessment is A20 id is 103 etc....
D/DBINFO: PerformanceAssessment = P20 id is 2 references Asessment with id 103
D/DBINFO: No ObjectiveAssessment.
using App Inspection then the database looks like :-
See how PerformanceAssessment with an assessment_id of 2 (it's unqiue row identifier) has an assessment_reference value of 103. This says the the Assessment with an assessment_id of 103 is the parent (or this PerformanceAssessment relates(belongs) to the Assessment that has an assessment_id of 103).

#Transactional does not work as expected, because the "save" method is needed to save to the database

#Transactionalshould itself reflect the changes made to the entity in the database.
I'm creating an application where the client can create a Car entity that looks like this (the update method is later used by PUT, do not pay attention to the brand property):
#Entity
#Table(name = "cars")
public class Car {
#Id
#GeneratedValue(generator = "inc")
#GenericGenerator(name = "inc", strategy = "increment")
private int id;
#NotBlank(message = "car name`s must be not empty")
private String name;
private LocalDateTime productionYear;
private boolean tested;
public Car() {
}
public Car(#NotBlank(message = "car name`s must be not empty") String name, LocalDateTime productionYear) {
this.name = name;
this.productionYear = productionYear;
}
#ManyToOne
#JoinColumn(name = "brand_id")
private Brand brand;
public int getId() {
return id;
}
void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDateTime getProductionYear() {
return productionYear;
}
public void setProductionYear(LocalDateTime productionYear) {
this.productionYear = productionYear;
}
public boolean isTested() {
return tested;
}
public void setTested(boolean tested) {
this.tested = tested;
}
public Brand getBrand() {
return brand;
}
void setBrand(Brand brand) {
this.brand = brand;
}
public Car update(final Car source) {
this.productionYear = source.productionYear;
this.brand = source.brand;
this.tested = source.tested;
this.name = source.name;
return this;
}
}
In my application, the client can create a new Car or update an existing one with the PUT method.
My controller:
#RestController
public class CarController {
private Logger logger = LoggerFactory.getLogger(CarController.class);
private CarRepository repository;
public CarController(CarRepository repository) {
this.repository = repository;
}
//The client can create a new resource or update an existing one via PUT
#Transactional
#PutMapping("/cars/{id}")
ResponseEntity<?> updateCar(#PathVariable int id, #Valid #RequestBody Car source) {
//update
if(repository.existsById(id)) {
repository.findById(id).ifPresent(car -> {
car.update(source); //it doesn`t work
//Snippet below works
//var updated = car.update(source);
//repository.save(updated);
});
return ResponseEntity.noContent().build();
}
//create
else {
var result = repository.save(source);
return ResponseEntity.created(URI.create("/" + id)).body(result);
}
}
}
When I create a new Car, it works. However as described in the code, when there is no save method the entity is not changed although I get the status 204 (no content). When there is a save method, it works fine.
Do you know why this is so?
One of the users asked me for a Brand entity. I haven't created any Brand object so far but essentially Car can belong to a specific Brand in my app. So far, no Car belongs to any Brand. Here is this entity:
#Entity
#Table(name = "brands")
public class Brand {
#Id
#GeneratedValue(generator = "i")
#GenericGenerator(name = "i", strategy = "increment")
private int id;
#NotBlank(message = "brand name`s must be not empty")
private String name;
private LocalDateTime productionBrandYear;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "brand")
private Set<Car> cars;
#ManyToOne
#JoinColumn(name = "factory_id")
private Factory factory;
public Brand() {
}
public int getId() {
return id;
}
void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDateTime getProductionBrandYear() {
return productionBrandYear;
}
public void setProductionBrandYear(LocalDateTime productionBrandYear) {
this.productionBrandYear = productionBrandYear;
}
public Set<Car> getCars() {
return cars;
}
public void setCars(Set<Car> cars) {
this.cars = cars;
}
public Factory getFactory() {
return factory;
}
public void setFactory(Factory factory) {
this.factory = factory;
}
}
I tried your entities with same use case locally and found out everything is working fine, I am writing here my findings and configurations so that you can verify what's going on wrong for you.
So, when I issue a PUT call providing id but Car entity doesn't exist into table, it gets created and I receive 201 response (I guess you are getting the same)
you can see that row with value got inserted into table as well
and these are the query logs printed
- [nio-8080-exec-8] org.hibernate.SQL: select count(*) as col_0_0_ from car car0_ where car0_.id=?
[nio-8080-exec-8] org.hibernate.SQL: select car0_.id as id1_1_0_, car0_.brand_id as brand_id5_1_0_, car0_.name as name2_1_0_, car0_.production_year as producti3_1_0_, car0_.tested as tested4_1_0_ from car car0_ where car0_.id=?
[nio-8080-exec-8] org.hibernate.SQL: insert into car (brand_id, name, production_year, tested) values (?, ?, ?, ?)
Now, let's come to updating the same entity, when issued PUT request for same id with changed values notice that values changes in table and update queries in log
You can see that got same 204 response with empty body, let's look the table entry
So changes got reflected in DB, let's look at the SQL logs for this operation
select count(*) as col_0_0_ from car car0_ where car0_.id=?
[nio-8080-exec-1] org.hibernate.SQL: select car0_.id as id1_1_0_, car0_.brand_id as brand_id5_1_0_, car0_.name as name2_1_0_, car0_.production_year as producti3_1_0_, car0_.tested as tested4_1_0_, brand1_.id as id1_0_1_, brand1_.name as name2_0_1_, brand1_.production_year as producti3_0_1_ from car car0_ left outer join brand brand1_ on car0_.brand_id=brand1_.id where car0_.id=?
[nio-8080-exec-1] org.hibernate.SQL: update car set brand_id=?, name=?, production_year=?, tested=? where id=?
So, I am not sure, how you verified and what you verified but your entities must work, I have used same controller function as yours
#RestController
class CarController {
private final CarRepository repository;
public CarController(CarRepository repository) {
this.repository = repository;
}
#PutMapping("/car/{id}")
#Transactional
public ResponseEntity<?> updateCar(#PathVariable Integer id, #RequestBody Car source) {
if(repository.existsById(id)) {
repository.findById(id).ifPresent(car -> car.update(source));
return ResponseEntity.noContent().build();
}else {
Car created = repository.save(source);
return ResponseEntity.created(URI.create("/" + created.getId())).body(created);
}
}
}
Possible differences from your source code could be as follow:
I used IDENTITY generator to generate the PRIMARY KEY, instead of the one you have on your entity as it was easy for me to test.
I provided ObjectMapper bean to serialize/deserialize the request body to Car object to support Java 8 LocalDateTime conversion, you may have your way to send datetime values, so that it converts to Car Object.
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
// And Object mapper bean
#Bean
public static ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return mapper;
}
However, these differences should not matter.
application.properties
To print query logs to verify if queries are fired or not
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=test
spring.datasource.password=test
spring.datasource.jpa.show-sql=true
spring.jpa.open-in-view=false
logging.level.org.hibernate.SQL=DEBUG
The fact that you are updating the car object doesn't mean it updates the value in the DB. You always need to call repository.save() method to persist your changes in the DB.

Android Room Test Delete Not Working (Java)

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.

Android Room insertAll issues

I'm starting using Android Room and I'm having some troubles.
I have an ArrayList of 7 Orders and when I call insertAll(List orders)
only 4 orders are inserted into the database.
How can I debug the insert query in order to find what is blocking ?
Thanks
The calls done by Room are not synchronous so probably when you perform
List<Order> myOrders = mDb.getOrderDao().getAll()
it is still inserting the orders.
Try this
#Dao
public interface OrderDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
public long insertOrder(Order order);
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAllOrders(List<Order> order);
#Query("SELECT * FROM orders")
List<Order> getAll();
}
#Entity(tableName = TABLE_NAME)
public class Order {
public static final String TABLE_NAME = "orders";
#PrimaryKey (autoGenerate = true)
private int id;
#SerializedName("order_number")
#ColumnInfo(name = "order_number")
private String orderNumber;
}
// Call
mDb.getOrderDao().insertAllOrders(orders);
Log.d(TAG, "inserted all");
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
#Override
public void run() {
List<Order> myOrders = mDb.getOrderDao().getAll();
Log.d(TAG, "Orders nr = " + myOrders.size());
}
});
OnConflict Property must be added.
#Insert(onConflict = OnConflictStrategy.REPLACE)

Hibernate call DELETE from table after method end

I have problem, and I don't know how to solve it.
I have entity:
#Entity
#Table(name = "entity_languagetree")
#AttributeOverride(name = "id", column = #Column(name = "languagetree_id"))
public class LanguageTree extends BaseObject {
#ElementCollection(targetClass = java.lang.String.class, fetch = FetchType.EAGER)
#CollectionTable(name = "view_languagetree_to_stringlist")
private List<String> relationship = new ArrayList<>();
public LanguageTree() {
//
}
public List<String> getRelationship() {
return relationship;
}
public void setRelationship(List<String> relationship) {
this.relationship = relationship;
}
}
where BaseObject is
#MappedSuperclass
public class BaseObject {
#Id
#GeneratedValue
#Column(name = "entity_id")
private Long id;
/**
*
* #return true if the entity hasn't been persisted yet
*/
#Transient
public boolean isNew() {
return id == null;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Bean getBean() {
return null;
}
}
Work with object - in my servlet, I am calling jsVarTree() like this:
String var = jsVarTree();
My problem is, that after method jsVarTree is finished, hibernate delete my relationship list from entity LanguageTree. I don't know why! I am not calling any delete and etc.. (I AM SURE, I SPENT A LOT OF TIME IN DEBUGER!)
:
#Override
public String jsVarTree() {
TreeBuilder tb = new TreeBuilder(getLanguageList());
return tb.getJsVarString(); // THIS METHOD IS ONLY GETTER !!!!
}
#Override
public List<String> getLanguageList() {
LanguageTree lt = getLanguageTreeObject();
return lt.getRelationship();
}
#Override
public LanguageTree getLanguageTreeObject() {
long fakingId = languageTreeDao.getLastId();
ServerLogger.logDebug("LAST FAKING ID: " +fakingId);
return languageTreeDao.findOne(fakingId);
}
I found this log in loggor:
HibernateLog --> 15:01:03 DEBUG org.hibernate.SQL - delete from
view_languagetree_to_stringlist where LanguageTree_languagetree_id=?
Can somebody tell me, why hibernate call delete over my table?
I saw a table in phpmyadmin..
TABLE IS FULL.
String var = jsVarTree();
TABLE IS EMPTY.
Table is deleted after return tb.getJsVarString(); is finished.
Thank you for any help!

Categories