如何为具有多种语言的词汇列表应用程序制作数据库,例如谷歌翻译?

Posted

技术标签:

【中文标题】如何为具有多种语言的词汇列表应用程序制作数据库,例如谷歌翻译?【英文标题】:How to make database for Vocabulary list app with multiple languages like google translate one? 【发布时间】:2021-11-02 11:44:56 【问题描述】:

我正在开发一个词汇列表应用程序,让我们考虑一下我的应用程序有 2 个用于多种语言的单词的输入。 例如,用户母语是韩语,想学英语,所以他/她会从应用设置中选择(韩语/英语),学习一个新单词,然后将韩语单词输入第一个输入,第二个输入英文含义输入并保存以稍后查看他/她的单词列表。 我为此做了一个简单的数据库和模型

@Entity(tableName = "words")
data class Word(
    val first_word: String,
    val second_word: String,
    val bookmarked: Boolean,
    @PrimaryKey(autoGenerate = true)
    val id: Int
)

但我的问题是如何更改它以支持多种语言但不在一个列表中显示它。 韩语/英语单词将显示在他们的列表中。 韩语/日语单词将显示在他们的列表中,并且... 这有点像谷歌翻译。您选择韩语,然后选择英语,它只会以韩语和英语向您显示单词。 我不知道如何制作这个数据库,我是否要为每 2 种语言制作多个表? 我希望你明白我想做什么:) 如果您需要更多信息,请告诉我。

另外,我正在使用 android/kotlin、SQLite 数据库和 Room 库。

【问题讨论】:

【参考方案1】:

在着手处理代码之前,您确实需要考虑设计。

我建议不需要多个表,并且可能会成为一个障碍。一个表,至少对于单词,而不是每个语言组合的多个表是可能的。通过指示 from 语言和 to 语言(语言由语言表指定)。

作为一个例子,不妨考虑以下 SQLite 代码:-

CREATE TABLE IF NOT EXISTS language (id INTEGER PRIMARY KEY, language TEXT UNIQUE);
CREATE TABLE IF NOT EXISTS wordv1 (id INTEGER PRIMARY KEY, fromLanguage INTEGER REFERENCES language(id), toLanguage INTEGER REFERENCES language(id), fromword TEXT, toword TEXT);

INSERT OR IGNORE INTO language (language) 
    VALUES('English'),('French'),('German')
;
INSERT OR IGNORE INTO wordv1 (fromlanguage,tolanguage,fromword,toword)
    VALUES
        (1,2,'Bread','Pain'),
        (1,3,'Bread','Brot'),
        (1,3,'Milk','Milch'),
        (1,2,'Milk','Lait'),
        (1,2,'Water','Eau'),
        (1,3,'Water','Wasser'),
        (3,1,'Haus','House'),
        (2,1,'Maison','House')
;

SELECT fromword, toword FROM wordv1 WHERE (tolanguage = 1 OR fromlanguage = 1) AND (tolanguage = 3 OR fromlanguage = 3);
SELECT 表示选择具有英语和德语成分的单词结果是:-

但是,上述内容未标准化,例如每次排列都会重复牛奶。

要规范化并让单词只有一个条目,需要多对多关系,这通常通过映射表实现(许多其他名称,例如交叉引用表)

要转移到这样的模式,单词表本身就更简单了,只有单词和它的语言。然后,映射表会将一个单词与另一种语言的另一个单词进行映射/关联/交叉引用。

语言表仍将按原样使用。

单词(wordv2)表可以简化为:-

 CREATE TABLE IF NOT EXISTS wordv2 (id INTEGER PRIMARY KEY, word TEXT, language INTEGER REFERENCES language(id));

在这个阶段,可以将单词加载到表中,以便复制上述内容(3 种语言的 4 个单词)然后:-

INSERT INTO wordv2 (word,language)
    VALUES
    ('Bread',1),('Milk',1),('Water',1),('House',1),
    ('Pain',2),('Lait',2),('Eau',2),('Maison',2),
    ('Brot',3),('Milch',3),('Wasser',3),('Haus',3)
;

映射表有两列,每列引用一个单词,主键(意味着唯一值)将由两列组合而成。所以这可能是:-

CREATE TABLE IF NOT EXISTS wordtranslatemap (fromword INTEGER REFERENCES wordv2(id),toword INTEGER REFERENCES wordv2(id), CHECK (fromword <> toword), PRIMARY KEY(toword,fromword));

将数据加载到映射表中,(包括双向转换(可能有用也可能没用),例如 Milk->Milch 和 Milch->Milk)可能是:-

INSERT INTO wordtranslatemap VALUES
    (1,5),(5,1),(1,9),(9,1),(5,9),(9,5),
    (2,6),(6,2),(2,10),(10,2),(6,10),(10,6),
    (3,7),(7,3),(3,11),(11,3),(7,11),(11,7),
    (4,8),(8,4),(4,12),(12,4),(8,12),(12,8)
;

请注意,上面的数字是 id 列,假设它们是连续的,从 1 开始,每次插入递增 1。 你不会/不应该对应用做出这样的假设,通常你会通过呈现的用户友好数据(例如单词或语言)来获取 id。

由于 Bread 是插入的第一行,它的 id 很可能是 1, Milk 2 .... ,因此 (1 (Bread), 5 (Pain)) 是 Bread 5,1 的英语到法语法语到英语(也许是可选的,但它可以让生活更轻松)。

检索数据有点复杂,例如,您可以使用以下内容:-

SELECT f.word, fl.language, t.word AS , tl.language /* The columns that we want NOTE use of aliases for tables */
FROM wordtranslatemap  /* the main table being looked at */
JOIN wordv2 AS f ON wordtranslatemap.toword = f.id /* get the referenced fromWord. give table an alias of f */
JOIN wordv2 AS t ON wordtranslatemap.fromword = t.id /* get the referenced toWord. give table an alias of f */
JOIN language AS fl ON f.language = fl.id /* get the language of the fromWord. alias with fl */
JOIN language AS tl ON t.language = tl.id /* get the language of the toWord. alias with tl */
/*
    Alias's required otherwise column references are ambiguous.
    See how in the output (as output columns not aliased, they could be) you have word and word(1)
    i.e. both output columns are named word the utility has detected them and added a distinction
*/
/* So we want the translation from English to German of all from words that start with Br */
WHERE fl.language = 'English' AND tl.language = 'German' AND f.word Like('Br%')
ORDER BY fl.id,tl.id,f.word,t.word
;

结果是:-

当您设计好架构(其中的表和列)后,您就可以将架构实施到房间中。

我建议始终使用唯一的列名

将上述内容放入 Room(第二次标准化)。

第 1 步 - 创建 实体(表格对象和表格)

语言表:-

@Entity
data class Language(
    @PrimaryKey
    @ColumnInfo(name = "language_id")
    var id: Long?=null,
    var language: String = ""
)

Word 表:-

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Language::class,
            parentColumns = ["language_id"],
            childColumns = ["language_map"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ],
    indices = [
        Index("language_map")
    ]
)
data class Word(
    @PrimaryKey
    @ColumnInfo(name = "word_id")
    var id: Long?=null,
    var word: String="",
    @ColumnInfo(name = "language_map")
    var language: Long
)

wordtranslatemap 表:-

@Entity(
    primaryKeys = ["from_word_map","to_word_map"],
    foreignKeys = [
        ForeignKey(
            entity = Word::class,
            parentColumns = ["word_id"],
            childColumns = ["from_word_map"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Word::class,
            parentColumns = ["word_id"],
            childColumns = ["to_word_map"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ],
    indices = [
        Index("to_word_map")
    ]
)
data class WordTranslateMap(
    @ColumnInfo(name = "from_word_map")
    val fromWord: Long,
    @ColumnInfo(name = "to_word_map")
    val toWord: Long
)

接下来是一种提取数据的方法,它结合了来自多个表的数据。这些是 POJO 类,即它们没有 @Entity,因为它们不是表。

虽然没有在示例中使用,但通过@Embedded 和@Relation WordWithLanguage 使用其语言的单词:-

data class WordWithLanguage (
    @Embedded
    val word: Word,
    @Relation(entity = Language::class,parentColumn = "word_id",entityColumn = "language_id")
    val language: Language
)
在这种情况下,@Relation 注释执行底层 JOIN(实际上它没有,但它通过底层查询模仿 JOIN)

为了反映上面的查询,TranslatedWord 的另一个 POJO :-

data class TransaltedWord(
    val from_word: String,
    val from_word_id: Long,
    val from_language: String,
    val from_language_id: Long,
    val to_word: String,
    val to_word_id: Long,
    val to_language: String,
    val to_language_id: Long
)
请注意,表中的实际列名没有被使用,所以我们处理不明确的列名。 列名将分别在各自的@Query 中分配

现在我们需要告诉 Room 将与数据库中的表交互的方法/函数。这些定义在使用@Dao 注释的接口或抽象类中。对于此示例,单个类 WordConvertDao 是:-

@Dao
abstract class WordConvertDao 
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(language: Language): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(word: Word): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(wordTranslateMap: WordTranslateMap): Long
    @Query("SELECT * FROM word")
    abstract fun getAllWords(): List<Word>
    @Query("SELECT * FROM language")
    abstract fun getAllLanguages(): List<Language>
    @Transaction
    @Query("SELECT * FROM wordtranslatemap")
    abstract fun getAllWordTranslateMaps(): List<WordTranslateMap>
    @Query("SELECT * FROM word")
    abstract fun getAllWordsWithLanguage(): List<WordWithLanguage>
    @Query("SELECT " +
            "f.word AS from_word, " +
            "f.word_id as from_word_id, " +
            "fl.language AS from_language, " +
            "fl.language_id AS from_language_id, " +
            "t.word AS to_word, " +
            "t.word_id AS to_word_id, " +
            "tl.language AS to_language, " +
            "tl.language_id AS to_language_id " +
            "FROM wordtranslatemap " +
            "JOIN word AS f ON from_word_map = f.word_id " +
            "JOIN word AS t ON to_word_map = t.word_id " +
            "JOIN language AS fl ON f.language_map = fl.language_id " +
            "JOIN language AS tl ON t.language_map = tl.language_id ")
    abstract fun getAllTranslatedWords(): List<TransaltedWord>

正如您所见/猜测的,@Insert 用于插入数据,@Query 用于提取数据。最后一个 @Query 与上面的 SQLite 查询等效,注意使用 AS 为列名起别名以适应 TranslatedWord POJO 的字段/成员/变量。

然后必须通过带有@Database 注释的抽象类将所有内容放在一起,在本例中为 TheDatabase :-

@Database(entities = [Language::class,Word::class,WordTranslateMap::class],version = 1)
abstract class TheDatabase: RoomDatabase() 
    abstract fun getWordConvertDao(): WordConvertDao

    @TypeConverters(Converters::class)

    companion object 
        @Volatile
        private var instance: TheDatabase? = null

        fun getInstance(context: Context): TheDatabase 
            if (instance == null) 
                instance = Room.databaseBuilder(context,TheDatabase::class.java,"wordconvert.db")
                    .allowMainThreadQueries()
                    .build()
            
            return instance as TheDatabase
        
    

注意 .allowMainThreadQueries 用于简洁/方便,建议您考虑使用主线程以外的其他方法。

最后,实际做点什么,加载一些数据,然后在 Activity MainActivity 中提取数据:-

class MainActivity : AppCompatActivity() 
    lateinit var db: TheDatabase
    lateinit var dao: WordConvertDao
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = TheDatabase.getInstance(this)
        dao = db.getWordConvertDao()

        val english = dao.insert(Language(language = "English"))
        val german = dao.insert(Language(language = "German"))
        val french = dao.insert(Language(language = "French"))
        val bread = dao.insert(Word(word = "Bread",language = english))
        val milk = dao.insert(Word(word = "Milk", language = english))
        val water = dao.insert(Word(word = "Water", language = english))
        val house = dao.insert(Word(word = "House", language = english))
        val brot = dao.insert(Word(word = "Brot",language = german))
        val milch = dao.insert(Word(word = "Milch", language = german))
        val wasser = dao.insert(Word(word = "Wasser",language = german))
        val haus = dao.insert(Word(word = "Hause", language = german))
        val pain = dao.insert(Word(word = "Pain", language = french))
        val lait = dao.insert(Word(word = "Lait", language = french))
        val eau = dao.insert(Word(word = "Eau", language = french))
        val maison = dao.insert(Word(word = "Maison", language = french))

        dao.insert(WordTranslateMap(bread,brot))
        dao.insert(WordTranslateMap(brot,bread))
        dao.insert(WordTranslateMap(bread,pain))
        dao.insert(WordTranslateMap(pain,bread))
        dao.insert(WordTranslateMap(pain,brot))
        dao.insert(WordTranslateMap(brot,pain))
        dao.insert(WordTranslateMap(milk,milch))
        dao.insert(WordTranslateMap(milk,lait))
        dao.insert(WordTranslateMap(milch,lait))
        dao.insert(WordTranslateMap(milch,milk))
        dao.insert(WordTranslateMap(lait,milk))
        dao.insert(WordTranslateMap(lait,milch))
        // etc

        // Extract everthing as TranslatedWord's and write the extracted data to the log.
        for(t: TransaltedWord in dao.getAllTranslatedWords()) 
            Log.d("CONVERTINFO","From=$t.from_word ($t.from_language) is $t.to_word ($t.to_language)")
        
    

结果:-

D/CONVERTINFO: From=Bread (English) is Brot (German)
D/CONVERTINFO: From=Brot (German) is Bread (English)
D/CONVERTINFO: From=Bread (English) is Pain (French)
D/CONVERTINFO: From=Pain (French) is Bread (English)
D/CONVERTINFO: From=Pain (French) is Brot (German)
D/CONVERTINFO: From=Brot (German) is Pain (French)
D/CONVERTINFO: From=Milk (English) is Milch (German)
D/CONVERTINFO: From=Milk (English) is Lait (French)
D/CONVERTINFO: From=Milch (German) is Lait (French)
D/CONVERTINFO: From=Milch (German) is Milk (English)
D/CONVERTINFO: From=Lait (French) is Milk (English)
D/CONVERTINFO: From=Lait (French) is Milch (German)

【讨论】:

感谢您的好回答,这正是我想要的。我有一些疑问。我们有每种语言的单词表还是所有语言的表?如果用户有大约 10000 字,那么查询不会造成任何不足或问题? @Morteza 10,000 字应该不是什么大问题。 to 和 from 词都被索引。但是,使用基于通配符(%_)的搜索(WHERE 子句)首先是通配符,将导致全表扫描而不是基于索引的搜索,因此速度会变慢。当然,影响性能的因素很多,很多是设备级别的。说一万行不算大。 嗨。我有另一个问题。所以我使用了您的代码并且它有效,但是在 TheDatabase 中,您编写了此代码 @TypeConverters(Converters::class) 而我没有那个类,也不知道为什么会出现。有必要吗?当然,代码在没有它的情况下仍然可以工作。我还尝试将您的代码更改为我的表单并使用视图模型和存储库,并且我还向 WordTranslateMap 实体添加了一些其他输入。所以我遇到了 ForeignKey 的问题,错误日志是:FOREIGN KEY 约束失败(代码 787 SQLITE_CONSTRAINT_FOREIGNKEY)。如何解决该错误? @Mortexa @TypConverters.... 行不是必需的,那是因为我使用了一个现有项目(不应该在代码中包含该行,我很抱歉)。 @Morteza,如果你有问题,那么你可能想问另一个问题。显示发生错误的代码以及存在的数据和正在插入/更改(可能正在插入)的数据。

以上是关于如何为具有多种语言的词汇列表应用程序制作数据库,例如谷歌翻译?的主要内容,如果未能解决你的问题,请参考以下文章

如何为数据框列表制作条形图?

如何为具有多种颜色颤动的文本颜色设置动画

如何为逐步连续上传用例制作 API 端点?

如何为gps创建折线列表?

如何为具有多种父类型的子场景编写 EF 代码优先映射

如何为 ChatApp 制作 ListView 来管理所有聊天?