זהו פרק הסיום. הוא מתחיל מן המודל וה-seed העובדים של
פרק 2 — קשר עם דירוג, מחליף את
relationship navigation ב-INNER JOIN typed ומוסיף שלושה כפתורים: Student,
Movie ו-Watching. לא נשנה שוב את ה-schema או את Database.java.
השינוי כולו בשלושה קבצים
| קובץ | שינוי |
|---|---|
strings.xml |
שלושה טקסטים לכפתורים |
activity_main.xml |
שורת כפתורים אחת |
MainActivity.java |
JOIN, listeners ושלושה דיאלוגים |
Gradle, קטלוג הספריות, Manifest, themes, קובצי הבדיקות, Database.java וכל
שלוש ה-entities נשארים ללא שינוי.
1. מוסיפים שלושה כפתורים
1.1 טקסטים
ב-app > res > values > strings.xml, הוסיפו:
<resources>
<string name="app_name">sqlrequery</string>
+ <string name="add_student">Add Student</string>
+ <string name="add_movie">Add Movie</string>
+ <string name="add_watching">Add Watching</string>
</resources>
1.2 שורת הכפתורים
ב-activity_main.xml, הוסיפו את ה-LinearLayout הבא לפני ה-TextView הקיים:
tools:context=".MainActivity">
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <Button
+ android:id="@+id/button_add_student"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/add_student" />
+
+ <Button
+ android:id="@+id/button_add_movie"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/add_movie" />
+
+ <Button
+ android:id="@+id/button_add_watching"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/add_watching" />
+ </LinearLayout>
+
<TextView
android:id="@+id/results"
layout_width="0dp" יחד עם layout_weight="1" מחלק את רוחב השורה שווה בשווה
בין שלושת הכפתורים. ה-ids ייצרו את binding.buttonAddStudent,
binding.buttonAddMovie ו-binding.buttonAddWatching.
2. מכינים את MainActivity
פתחו app > kotlin+java > com.example.sqlrequery > MainActivity.
2.1 imports
הוסיפו את imports של רכיבי הדיאלוג:
import android.os.Bundle;
+import android.text.InputType;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
הרחיבו את imports של המודל והוסיפו את שתי רשימות Java:
import com.example.sqlrequery.databinding.ActivityMainBinding;
+import com.example.sqlrequery.model.Movie;
+import com.example.sqlrequery.model.MovieEntity;
+import com.example.sqlrequery.model.Student;
+import com.example.sqlrequery.model.StudentEntity;
import com.example.sqlrequery.model.Watching;
import com.example.sqlrequery.model.WatchingEntity;
+import java.util.ArrayList;
+import java.util.List;
לבסוף הוסיפו את הטיפוסים הדרושים ל-JOIN ולטיפול בכפילות:
import io.requery.Persistable;
+import io.requery.PersistenceException;
import io.requery.query.Result;
+import io.requery.query.Tuple;
import io.requery.sql.EntityDataStore;
הוסיפו תיאור קצר למחלקה:
+/** Displays the typed join and owns the three insert dialogs. */
public class MainActivity extends AppCompatActivity {
2.2 listeners
בתוך onCreate, מיד לאחר פתיחת המסד וה-seed, חברו את הכפתורים. השאירו את
showWatchings() אחרונה:
data = Database.open(this);
Database.seedIfEmpty(data);
+
+binding.buttonAddStudent.setOnClickListener(v -> showAddStudent());
+binding.buttonAddMovie.setOnClickListener(v -> showAddMovie());
+binding.buttonAddWatching.setOnClickListener(v -> showAddWatching());
showWatchings();
3. מחליפים navigation ב-INNER JOIN typed
בפרק 2 התחלנו מכל Watching ועברנו דרך האובייקטים הקשורים. כעת נבקש מ-SQLite
לבצע את החיבור ולשלוח בחזרה רק את שלושת הערכים שאנחנו מציגים.
SQL
SELECT s.name, m.title, w.rating
FROM Student s
INNER JOIN Watching w
ON w.studentId = s.id
INNER JOIN Movie m
ON m.id = w.movieId
ORDER BY s.id, m.id;
Requery typed
data.select(
StudentEntity.NAME,
MovieEntity.TITLE,
WatchingEntity.RATING)
.from(Student.class)
.join(Watching.class)
.on(WatchingEntity.STUDENT_ID
.eq(StudentEntity.ID))
.join(Movie.class)
.on(MovieEntity.ID
.eq(WatchingEntity.MOVIE_ID))
.orderBy(StudentEntity.ID, MovieEntity.ID)
אין כאן שמות טבלאות או עמודות כמחרוזות. כל expression נוצר מ-Java Entity, ולכן טעות בשם או בטיפוס נעצרת בזמן קומפילציה.
החליפו את showWatchings:
-/** Displays every rated relationship through its Student and Movie objects. */
+/** Runs the typed equivalent of two INNER JOINs and displays every result tuple. */
private void showWatchings() {
StringBuilder text = new StringBuilder("STUDENT | MOVIE | RATING\n\n");
- try (Result<Watching> watchings = data.select(Watching.class)
- .orderBy(WatchingEntity.STUDENT_ID, WatchingEntity.MOVIE_ID)
+ try (Result<Tuple> rows = data
+ .select(StudentEntity.NAME, MovieEntity.TITLE, WatchingEntity.RATING)
+ .from(Student.class)
+ .join(Watching.class).on(WatchingEntity.STUDENT_ID.eq(StudentEntity.ID))
+ .join(Movie.class).on(MovieEntity.ID.eq(WatchingEntity.MOVIE_ID))
+ .orderBy(StudentEntity.ID, MovieEntity.ID)
.get()) {
- for (Watching watching : watchings) {
- text.append(watching.getStudent().getName())
+ for (Tuple row : rows) {
+ text.append(row.get(StudentEntity.NAME))
.append(" | ")
- .append(watching.getMovie().getTitle())
+ .append(row.get(MovieEntity.TITLE))
.append(" | ")
- .append(watching.getRating())
+ .append(row.get(WatchingEntity.RATING))
.append('\n');
}
}
בחירה של כמה expressions מחזירה Tuple. כל ערך נקרא באמצעות אותו expression
שבחר אותו: StudentEntity.NAME, MovieEntity.TITLE או
WatchingEntity.RATING.
אפשר לבצע כאן Build > Make Project ולהריץ. המסך עדיין יציג אותן ארבע שורות,
אבל Logcat יכיל כעת שני inner join-ים. שינוי דרך השליפה לא שינה את התוצאה.
4. בונים טפסי דיאלוג קטנים
שלושת הדיאלוגים צריכים כמה Views פשוטים. במקום ליצור שלושה קובצי XML נוספים,
נוסיף פעולות עזר קטנות בתחתית MainActivity:
/** Creates one plain single-line dialog input. */
private EditText field(String hint) {
EditText field = new EditText(this);
field.setHint(hint);
field.setSingleLine();
return field;
}
/** Creates a small label for a dialog field. */
private TextView label(String text) {
TextView label = new TextView(this);
label.setText(text);
return label;
}
/** Creates a Spinner that displays the supplied strings. */
private Spinner spinner(List<String> values) {
Spinner spinner = new Spinner(this);
spinner.setAdapter(new ArrayAdapter<>(this,
android.R.layout.simple_spinner_dropdown_item, values));
return spinner;
}
/** Stacks dialog controls vertically with density-independent horizontal padding. */
private LinearLayout form(View... fields) {
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
int padding = (int) (24 * getResources().getDisplayMetrics().density);
form.setPadding(padding, 0, padding, 0);
for (View field : fields) form.addView(field);
return form;
}
/** Shows brief feedback without adding another persistent view to the screen. */
private void toast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
form(View... fields) מקבלת מספר משתנה של Views, מסדרת אותם אנכית ומוסיפה
padding שמומר מ-dp לפי צפיפות המכשיר. כך אותה פעולה משרתת את כל הדיאלוגים.
5. דיאלוג Student
הוסיפו מעל פעולות העזר:
/** Shows a one-field dialog and inserts a Student when its name is non-empty. */
private void showAddStudent() {
EditText name = field("Name");
new AlertDialog.Builder(this)
.setTitle("Add Student")
.setView(form(name))
.setNegativeButton("Cancel", null)
.setPositiveButton("Add", (dialog, which) -> {
String value = name.getText().toString().trim();
if (value.isEmpty()) {
toast("Enter a student name");
} else {
Database.addStudent(data, value);
toast("Student added");
}
})
.show();
}
הפעולה משתמשת ב-Database.addStudent שכבר כתבנו בפרק 1. תלמיד בלי Watching
עדיין לא יופיע בתוצאת ה-JOIN; נשתמש בו מיד בדיאלוג השלישי.
6. דיאלוג Movie
הוסיפו:
/** Shows a two-field dialog and inserts a Movie when both values are non-empty. */
private void showAddMovie() {
EditText title = field("Title");
EditText genre = field("Genre");
new AlertDialog.Builder(this)
.setTitle("Add Movie")
.setView(form(title, genre))
.setNegativeButton("Cancel", null)
.setPositiveButton("Add", (dialog, which) -> {
String movieTitle = title.getText().toString().trim();
String movieGenre = genre.getText().toString().trim();
if (movieTitle.isEmpty() || movieGenre.isEmpty()) {
toast("Enter a title and genre");
} else {
Database.addMovie(data, movieTitle, movieGenre);
toast("Movie added");
}
})
.show();
}
גם כאן אנחנו משתמשים בפעולת ה-insert שכבר קיימת. סרט ללא Watching עדיין אינו
מופיע ב-INNER JOIN.
7. דיאלוג Watching
הדיאלוג צריך לשמור את האובייקטים שנבחרו, אבל Spinner מציג טקסט. הוסיפו שתי פעולות שמייצרות תוויות באותו סדר של הרשימות המקוריות:
/** Returns Student names in the same order as their objects. */
private List<String> namesOf(List<Student> students) {
List<String> names = new ArrayList<>();
for (Student student : students) names.add(student.getName());
return names;
}
/** Returns Movie titles in the same order as their objects. */
private List<String> titlesOf(List<Movie> movies) {
List<String> titles = new ArrayList<>();
for (Movie movie : movies) titles.add(movie.getTitle());
return titles;
}
כעת הוסיפו את הדיאלוג:
/** Selects Student and Movie choices, then inserts one rated relationship. */
private void showAddWatching() {
List<Student> students;
List<Movie> movies;
try (Result<Student> rows = data.select(Student.class)
.orderBy(StudentEntity.NAME).get()) {
students = rows.toList();
}
try (Result<Movie> rows = data.select(Movie.class)
.orderBy(MovieEntity.TITLE).get()) {
movies = rows.toList();
}
Spinner student = spinner(namesOf(students));
Spinner movie = spinner(titlesOf(movies));
EditText rating = field("Rating (1-10)");
rating.setInputType(InputType.TYPE_CLASS_NUMBER);
new AlertDialog.Builder(this)
.setTitle("Add Watching")
.setView(form(label("Student"), student, label("Movie"), movie, rating))
.setNegativeButton("Cancel", null)
.setPositiveButton("Add", (dialog, which) -> {
int score;
try {
score = Integer.parseInt(rating.getText().toString());
} catch (NumberFormatException e) {
toast("Enter a rating from 1 to 10");
return;
}
if (score < 1 || score > 10) {
toast("Enter a rating from 1 to 10");
return;
}
try {
Database.addWatching(data,
students.get(student.getSelectedItemPosition()),
movies.get(movie.getSelectedItemPosition()), score);
showWatchings();
toast("Watching added");
} catch (PersistenceException e) {
toast("That student already watched that movie");
}
})
.show();
}
ה-index שנבחר בכל Spinner ניגש לאותו index ברשימת ה-entities. לכן
Database.addWatching מקבלת Student ו-Movie אמיתיים ולא רק את הטקסט שלהם.
rating מוגבלת ל-1–10 לפני ה-insert. אם אותו צמד Student–Movie כבר קיים,
המפתח הראשי המורכב של Watching גורם ל-PersistenceException, והאפליקציה מציגה
הודעה במקום לקרוס.
8. מריצים את המסלול המלא
בצעו Build > Make Project, הריצו ובצעו:
- לחצו Add Student, הזינו
Liaולחצו Add. - לחצו Add Movie, הזינו
Cocoו-Animation. - לחצו Add Watching, בחרו
Lia, בחרוCoco, הזינו7ולחצו Add.
השורה החמישית צריכה להופיע מיד:
Lia | Coco | 7
ב-Logcat אמורות להופיע פעולת insert אחת לכל טבלה ולאחר מכן שאילתה הכוללת:
inner join Watching ... inner join Movie ...
סגרו והריצו שוב. השורה החמישית צריכה להישאר, והפתיחה מחדש צריכה לבצע אפס inserts.
נסו להוסיף שוב Lia → Coco. במקום שורה כפולה אמורה להופיע ההודעה:
That student already watched that movie
רשימת בדיקה
- השתנו רק
strings.xml,activity_main.xmlו-MainActivity.java. - המסך הראשוני עדיין מציג את ארבע שורות ה-seed.
- Logcat מציג שני
INNER JOIN-ים. - ניתן להוסיף Student ו-Movie דרך הדיאלוגים.
- Watching חדש מרענן מיד את תוצאת ה-JOIN.
- rating מחוץ לטווח 1–10 אינו נשמר.
- צמד Student–Movie כפול אינו נשמר.
- השורה החדשה נשארת לאחר restart ללא seed נוסף.
סיימנו יישום Requery קטן מקצה לקצה: מודל Java יצר schema ב-SQLite, קשר עם מידע משלו נשמר במפתח מורכב, שאילתת JOIN נכתבה באמצעות expressions typed, ושלושת סוגי הרשומות ניתנים להוספה מן המסך.