Постройте настройку ORM от начала до конца

Обратите внимание: этот пост во многом основан на нескольких учебных модулях Flatiron School из раздела лабораторных работ и лекций ORM.

Это служит повторением этих модулей - для создания настройки ORM от начала до конца самым простым и подробным способом, который я могу объяснить. Из-за такого сложного уровня полировки эта статья получилась довольно длинной. Однако, тем не менее, он должен предоставлять массу информации тем, кто его читает.

Кроме того, в примерах этого поста в качестве версии SQL используется SQLite3. Логика, используемая в примерах, должна быть одинаковой, однако синтаксис может отличаться для разных версий SQL.

«ORM - это метод доступа к реляционной базе данных с использованием объектно-ориентированного языка. Он позволяет языку управлять данными базы данных путем «сопоставления» таблиц базы данных с классами. Строки создаются из экземпляров этих классов. »- Перефразировано из учебного модуля« Почему полезен ORM ».

ORM - объектно-реляционное отображение

Что именно это означает? Другой способ описания ORM состоит в том, что с помощью этого метода вы можете создать «базу данных виртуальных объектов», которую можно использовать и управлять ею из объектно-ориентированного языка (как сформулировано в статье на Sitepoint).

Поскольку данные обрабатываются в объекте по-разному, чем в базе данных, мы используем ORM для преодоления разрыва и установления связи между ними.

Его можно было рассматривать как своего рода переводчик. Допустим, вы находитесь на японском веб-сайте с английской клавиатурой. Вы загружаете расширение для перевода содержимого веб-сайта, а также переводите английские иероглифы на японские.

С помощью этого расширения вы можете видеть информацию, имеющуюся на веб-сайте, и впоследствии получать доступ к этой информации. В этом примере японский веб-сайт может быть эквивалентен базе данных, английская клавиатура - объектно-ориентированному языку (например, Ruby), а расширение переводчика - ORM.

ORM передает кодировку SQL на Ruby с повышенной эффективностью.

SQL может быть сложным языком для программирования, и, кроме того, многие его команды могут быть очень повторяющимися. Например, для добавления новых данных в базу данных требуется целая INSERT INTO-table-(parameters)-VALUES-(arguments) строка кода, которую необходимо писать каждый раз, когда вы хотите ввести новые данные.

Не обращайте внимания на тот факт, что у вас может быть надежная таблица с несколькими параметрами, все из которых вам придется вводить каждый раз, а затем их последующие значения.

Вместо того, чтобы писать тонны кода SQL, мы можем использовать ORM в качестве связующего звена. В классе мы можем создать self.save метод, который выглядит примерно так:

Переменная database_connection просто устанавливает соединение с нашей базой данных, в этом примере она называется ex_database.db.

Метод execute принимает в качестве аргумента инструкцию SQL и выполняет ее.

Знаки вопроса в операторе SQL представляют аргументы, которые необходимо вставить, а сами аргументы перечислены сразу после оператора для вставки.

Оттуда, используя переменную класса @@all, мы могли бы сделать что-то вроде:

Это позволяет нам вызывать метод save для всех экземпляров Class. Каждый экземпляр будет проходить через метод save, что означает, что параметры каждого экземпляра будут подключены к методу execute внутри метода save.

Таким образом, мы создаем новый оператор SQL для каждого экземпляра и его параметров и выполняем его.

В этом случае в нашем операторе SQL было INSERT INTO, поэтому мы вставили новые данные в ex_database.db. Это намного быстрее, чем вводить отдельные команды SQL по очереди.

Создание базы данных с помощью Ruby и ORM

Допустим, у нас есть база данных музыкальной библиотеки, содержащая песни и исполнителей. У каждой песни есть один исполнитель, но у исполнителя может быть несколько песен.

Чтобы создать базу данных, мы создадим environment.rb файл со следующим:

Сначала нам нужно создать музыкальную базу данных.

В файле Ruby нам потребуется использовать программу SQL (в данном случае SQLite3), а также любые файлы, которые мы хотим преобразовать в таблицы в этой базе данных. В нашем случае это .rb файлы, поэтому сначала нам нужно создать классы, хранящиеся в тех файлах, которые позже будут преобразованы. Важные для нас файлы называются song.rb и artist.rb.

Константа DB здесь установлена ​​как хэш - мы в основном говорим, что наше соединение (ключ) :conn будет переходить к новой базе данных с именем music.db (значение). Эта база данных будет доступна и изменена SQLite3.

Позже это можно будет вызвать в наших файлах song.rb и artist.rb, используя DB[:conn].

Помните, что когда мы вызываем ключ хеша, возвращается соответствующее значение, так что оно фактически указывает на базу данных music.db.

Наш следующий шаг - заполнить файлы song.rb и artist.rb. В этом примере мы будем работать с файлом song.rb, поскольку создание файла artist.rb будет происходить аналогично.

Сначала мы создаем Song класс.

Этот класс со временем превратится в стол. Традиционная структура ORM имеет свои таблицы с именами во множественном числе - таким образом, когда этот класс «конвертируется» в таблицу, он будет известен как таблица «песен».

Вы заметите, что этот класс во многом схож с классами, не связанными с ORM - он имеет attr_accessors с типичными свойствами, но также имеет атрибут :id.

Атрибут: id становится первичным ключом таблицы

Атрибут :id будет преобразован в первичный ключ, когда экземпляр Song будет перемещен в базу данных.

Он имеет значение по умолчанию nil, потому что при создании экземпляра его первичный ключ / идентификатор будет создан, когда этот экземпляр будет перемещен в базу данных.

Сами программисты не определяют первичный ключ / id. Мы позволяем ORM обрабатывать их назначение, потому что для нас было бы огромной проблемой отслеживать идентификатор каждой песни при создании нового экземпляра. Автоматизируя этот процесс с помощью ORM, мы получаем более быстрый и менее подверженный ошибкам способ назначения первичных ключей.

Фактически мы создаем таблицу песен (из класса Song), создавая метод self.create_table. Это метод класса, потому что для экземпляра не имеет смысла создавать таблицу - в противном случае у вас была бы таблица для каждого экземпляра, и это было бы в значительной степени бесполезно.

Создав create_table метод класса, мы касаемся всех экземпляров Song, и эти экземпляры позже будут сопоставлены с таблицей песен.

Мы используем строку sql = <<- SQL.

Синтаксис двойной стрелки <<- позволяет создавать heredoc, который является многострочным строковым литералом. Это позволяет пользователю вводить несколько строк в одну строку и позволяет им формировать новые строки там, где они пожелают.

Это полезно, потому что мы должны ввести здесь команду SQL - если бы мы попытались использовать обычную строку, она была бы очень длинной, и вам пришлось бы прокручивать вправо каждый раз, когда вас интересовало содержимое этой строки.

Heredocs начинаются и заканчиваются последовательностью, которая указывает начало и конец этой строки (примечание: в этом посте я могу назвать такие последовательности «фланкерными последовательностями» из-за того, что они окружают строку).

Здесь мы используем последовательность SQL перед началом строки и сразу после ее окончания. SQL - хорошая последовательность для использования здесь - она ​​явно указывает, с чем связано содержимое этой строки.

Код SQL, содержащийся в heredoc, говорит следующее: «Если нет таблицы с именем« песни », создайте ее с тремя следующими свойствами: идентификатор (первичный ключ), который является целым числом, имя, которое является текстом ( строка), и альбом, который также является текстом (строкой) ».

После всего этого мы можем фактически считать таблицу songs созданной. Таблица songs будет одной из нескольких таблиц в базе данных музыкальной библиотеки.

Помните, что база данных содержит несколько таблиц. Однако для наших целей мы сосредоточимся только на одном.

Как объектные данные отправляются в таблицу

Легко предположить, что весь объект Ruby отправляется в свою таблицу, находящуюся в общей базе данных. Однако это совсем не то, что происходит.

Как объяснялось ранее, база данных не распознает объекты Ruby. На самом деле происходит то, что атрибуты объекта пересылаются, составляя единственную строку в этой таблице.

В нашем примере это означает, что для Song @name и @artist отправляются в таблицу songs, не весь объект Song.

Чтобы быть более ясным, если бы у нас была переменная back_to_black, которая была экземпляром Song, с @name = «Back to Black» и @artist = «Amy Winehouse», это «Back to Black» и «Amy Winehouse», которые будут отправлено, а не сам объект back_to_black.

Визуально это выглядело бы так:

Возвращение к методу сохранения

Мы уже обсуждали, что должен делать метод save, но на этот раз мы рассмотрим его с точки зрения класса Song.

Опять же, мы устанавливаем var sql равным heredoc (используя последовательность «SQL» в качестве фланкерных последовательностей для многострочной строки), и наша строка представляет собой код SQL, говорящий: «Для значений (имя, альбом) в песнях таблицы, введите эти значения (значение1, значение2) ».

После этого мы обращаемся к константе БД и выполняем команду.

Помните, что ввод hash[:key] фактически указывает на значение этого ключа, поэтому на самом деле мы указываем на то, к чему привязана база данных :conn (music.db).

Когда мы запускаем метод .execute в этой базе данных, мы используем sql, self.name и self.album в качестве аргументов.

Помните, что мы используем sql в качестве локальной переменной в этом методе сохранения, а sql - это просто фрагмент кода SQL.

Итак, мы на самом деле говорим:

Допустим, мы еще ничего не отправили в базу music.db.

Итак, чтобы создать таблицу песен в базе данных, мы сначала вызовем Song.create_table.

Затем мы можем вызвать back_to_black.save, который примет значения @name и @artist back_to_black , и отправит их в таблицу песен в виде строки.

После создания этой строке автоматически назначается первичный ключ, расположенный под столбцом id таблицы songs.

Переназначение атрибута @id: получение первичного ключа из таблицы и установка его значения на @id

Пока мы не отправили наши данные в таблицу в базе данных, значение @id для back_to_black было nil.

В таблице ему автоматически присваивается значение в виде первичного идентификатора. Однако, вернувшись к Ruby, он все еще nil.

Итак, что нам нужно сделать - теперь, когда наша таблица существует и первичный ключ был назначен данным back_to_black, мы хотим получить этот первичный ключ и установить для него @id.

По сути, мы переназначаем @id из его предыдущего значения nil на новое значение первичного ключа из таблицы songs.

Для этого устанавливаем @id = DB[:conn].execute(“SELECT last_insert_rowid() FROM songs”)[0][0].

Этот код прямо говорит: «back_to_black, @id был nil раньше. Переназначьте его, войдя в базу данных music.db, а оттуда перейдите в таблицу песен. В этой таблице перейдите к строке, в которую мы только что вставили данные back_to_black. Из этого ряда возьмите его id. Это id - значение того, чему будет присвоен атрибут back_to_black @id ".

Причина, по которой мы должны написать [0][0], заключается в том, что данные, переданные из базы данных, возвращаются в виде массива в Ruby. Если возвращаются данные из нескольких строк, вы получите массив массивов в виде чего-то вроде [[row1.name, row1.artist] , [[row2.name, row2.artist]].

Внутри нашего возвращенного массива у нас будут данные строки, но только их часть. Итак, мы получили [[1]]. Чтобы получить доступ к 1, мы должны получить первый (и единственный) элемент из возвращенного массива, который является массивом. Затем мы получаем первый элемент этого массива, равный 1.

Создание новых экземпляров класса и отправка атрибутов в таблицу

Метод класса self.create - это эффективный способ создания новых экземпляров класса и передачи атрибутов указанного экземпляра в таблицу (при одновременном переназначении @id значения экземпляра).

Метод класса self.create суммирует многое из того, что мы уже сделали.

Цель этого метода - убрать процесс создания программистом нового экземпляра класса, а затем последующего запуска .save в этом классе и затем извлечения id из базы данных для присвоения этому классу. экземпляра @id.

Чтобы ускорить весь этот процесс, мы должны просто использовать метод!

Метод self.create принимает аргументы имени и альбома, которые будут использоваться в качестве аргументов для Song.new, а затем запускает .save на этом экземпляре.

Однако мы не хотим неявно возвращать результат .save, и мы определенно не хотим бегать и пытаться самостоятельно извлечь этот новый экземпляр из таблицы songs, поэтому мы просто возвращаем новый Song вместо этого.

Сопоставление таблиц базы данных с объектами Ruby

До сих пор мы видели, что можем создавать объекты Ruby, брать атрибуты их данных и помещать эти данные в таблицу в базе данных.

Мы также можем сделать обратное - то есть мы можем взять данные из уже существующей таблицы в установленной базе данных, используя ORM.

В этом примере давайте вспомним нашу back_to_black переменную - экземпляр класса Song с @name = «Back to Black» и @artist = «Amy Winehouse».

Хотя в настоящее время у нас все еще есть эта переменная, допустим, мы удалили ее по ошибке. К счастью, мы уже загрузили его в таблицу песен в базе данных music.db.

Однако мы должны вернуть его как объект Ruby. Как мы можем сделать это?

Таблицы хранят чистые данные, тогда как объект Ruby более сложен. Это означает, что нам придется воспользоваться преимуществами ORM и создать метод в Ruby для извлечения данных из таблицы и преобразования их во что-то значимое.

Метод self.new_from_db берет данные из таблицы и преобразует их в объект

Метод self.new_from_db является методом класса, потому что мы собираемся создать с его помощью новые экземпляры Song .

Сначала мы устанавливаем переменную равной new_song = Song.new.

Затем в следующих строках мы устанавливаем @id, @name и @artist to row[#] этого нового экземпляра.

Причина, по которой мы устанавливаем эти атрибуты равными массиву строк, заключается в том, что, когда данные извлекаются из SQLite обратно в Ruby, они возвращаются в виде массива.

Порядок в этом массиве отражает способ создания экземпляра класса - в нашем методе def initialize мы построили @id, @name и @artist именно в этом порядке, так что это порядок столбцов в таблице songs.

Метод self.all в ORM использует подход на основе SQL для извлечения данных из таблиц.

В SQL довольно просто вернуть все значения таблицы с помощью команды SELECT * FROM table.

В нашем Song.all методе мы используем это преимущество, используя переменную sql, установленную в heredoc, с этой командой SQL, окруженной последовательностью символов SQL.

Оттуда мы должны вызвать DB[:conn], чтобы установить соединение с базой данных music.db и, следовательно, с таблицей песен.

По сути, мы запускаем SELECT * FROM songs.

Помните, что SQLite возвращает данные в Ruby в виде массивов, поэтому в настоящее время мы собираемся получить массив только с одним массивом в нем, поскольку «Back to Black» было единственным, что мы добавили в таблицу songs. [ [1, “Back to Black”, “Amy Winehouse”] ]

Теперь у нас есть массив массивов. Это означает, что мы успешно получили некоторые данные Ruby, которые можно использовать для создания объекта.

Поэтому мы запускаем map в этом массиве вместе с методом new_from_db, создавая новый экземпляр Song и заполняя его данными, извлеченными из таблицы songs.

Например, первый элемент в нашем массиве строк - [1, “Back To Black”, “Amy Winehouse”]. Мы берем 1 и устанавливаем его как @id, мы берем «Back To Black» и устанавливаем его как @name, и мы берем «Amy Winehouse» и устанавливаем его как @artist.

Если бы у нас было больше элементов в массиве, мы бы сделали то же самое с ними до тех пор, пока не будет пройден весь массив массивов.

Self.find_by_name использует подход на основе SQL в сочетании с Ruby для создания нового экземпляра

Хотя метод self.all будет создавать новые экземпляры для каждой песни в таблице Songs, мы можем не захотеть делать это каждый раз.

Скажем, «Back to Black» был единственным песенным объектом, который мы хотели создать из множества песен в таблице песен.

Было бы расточительно и утомительно создавать все строки таблицы песен в элементы, а затем копаться в них, чтобы найти то, что мы хотели, поэтому вместо этого мы будем использовать SQL и Ruby, чтобы получить то, что мы хотим, с помощью мощи ORM.

В этом методе мы устанавливаем sql равным команде, которая по сути говорит следующее:

«Просмотрите всю таблицу песен и верните все строки данных для песни, название которой я вам говорю. Верни мне только одну песню ».

В этом случае мы собираемся сообщить SQL, что песня, которая нас интересует, - это «Back to Black», поэтому мы вернем массив [ [1, “Back to Black”, “Amy Winehouse”] ].

У нас снова есть этот массив массивов, и мы можем создать из него объект.

Мы подвергнем этот массив массивов .map и запустим на нем new_from_db, чтобы создать новый Song экземпляр с данными из таблицы песен, которые будут использоваться в качестве значений для его @id, @name и @artist.

Помните, что .map по-прежнему возвращает массив, поэтому вы получите что-то вроде [#<Song:0x083502585324de47de(1, “Amy Winehouse”, “Back to Black”)>].

Но нам не нужен массив, нам нужен объект внутри. Для этого все, что нам нужно сделать, это вызвать .first в этом массиве, и вуаля! У нас есть объект!

Обновление существующих записей в ORM

Во многих случаях данные являются динамическими. Вы можете быть в списке рассылки и сменить адрес, вы можете сменить сферу карьеры или даже музыку, которой вы увлекаетесь.

Из-за этого нам не только нужно создавать и извлекать данные, как мы делали раньше, но и нужно изменять, когда придет время. Для нас это время прямо сейчас.

Мы так долго занимаемся переменной «Back to Black», что можно подумать, что мы запомнили каждую ее ноту.

Но, допустим, когда мы пошли загружать эту песню, мы случайно назвали ее «Назад в будущее», и мы уже загрузили ее в таблицу песен. Независимо от почему это произошло, это необходимо исправить.

Мы можем получить эти данные с помощью метода find_by_name и установить информацию, которую мы возвращаем, в переменную, например:

back_to_black = Song.find_by_name(“Back to the Future”)

А затем мы можем изменить возвращаемый объект, применив к нему метод .name=.

Помните, что в нынешнем виде у нас все еще есть строка с надписью «Back to the Future» в таблице песен. Если мы обновим нашу новую переменную back_to_black в таблице, мы ничего не перезаписали, мы просто добавили новую песню Эми Уайнхаус.

Чтобы фактически изменить существующую строку, мы должны воспользоваться методом SQL UPDATE.

Этот метод делает именно то, что он говорит: как только вы укажете, где вы хотите что-то изменить в таблице, и, возможно, используете некоторые аргументы и / или условные выражения, он сделает именно это.

Чтобы выполнить это, мы создаем метод обновления, который говорит: «Ищите эту таблицу, мы собираемся ее обновить. Сбросьте значения имени и исполнителя для совпадающего идентификатора ».

Затем мы фактически выполняем эту команду, вызывая метод DB[:conn].execute.

Как избежать вставки повторяющихся данных в таблицу

Мы рассмотрели обновление данных, которые уже существуют в таблице, но мы все еще не решили проблему дублирования данных.

Например, если бы у нас уже было «Back to Black» в нашей таблице песен, ничто не мешает нам обновить то же самое снова - единственное, что будет отличаться между ними, - это их идентификаторы.

В настоящее время наш save метод отражает это.

Он всегда будет вводить новые данные, не проверяя, действительно ли эти данные «новые».

У нас есть INSERT INTO без какого-либо предложения, которое бы сообщало об этом: «вставлять только в том случае, если данные действительно новые». Итак, нам придется изменить его, чтобы он был более избирательным.

Для этого мы можем использовать if statement.

Вы могли подумать, что в SQL можно использовать предложение WHERE, но его написание может оказаться более сложным. Более того, мы даже не хотим использовать SQL, если данные, которые мы пытаемся загрузить, уже существуют. Скорее, мы сначала хотим увидеть, есть ли это в таблице.

Как это сделать без использования SQL?

Мы уже установили, что каждый раз, когда в таблицу загружается новый фрагмент данных, ему присваивается id, который мы сразу же получаем и переназначаем @id из nil на значение этого идентификатора.

Следовательно, мы можем составить if оператор, запрашивающий значение @id - все, что не nil или false, считается правдой и, следовательно, считается правдой.

Если оценка значения @id равна true, мы можем просто обновить данные в таблице с помощью нашего метода обновления, а не добавлять их снова.

Если @id по-прежнему равен нулю, будет запущен else statement, что означает, что мы следуем нашему исходному коду в методе save, создаем новую строку в таблице песен и извлекаем из нее идентификатор.

Заключение

Вот и все!

Это должно дать вам представление о том, как ORM работает с Ruby. Вы рассмотрели несколько методов, от создания таблицы до изменения ее данных и извлечения этих данных в объекты Ruby.

Таким образом, вы на правильном пути к созданию сложных программ!

Источники

  1. Learn.co Почему ORM полезен , Школа Flatiron, https://github.com/learn-co-students/ruby-orm-nyc-web- 060319
  2. Аджит, М. (2014). ORM в Ruby: введение Sitepoint, https://www.sitepoint.com/orm-ruby-introduction/
  3. Learn.co Сопоставление классов Ruby с таблицами баз данных, Flatiron School, https://github.com/learn-co-students/orm-mapping-to-tables-nyc-web-060319
  4. Learn.co Сопоставление таблиц базы данных с объектами Ruby, Flatiron School, https://github.com/learn-co-students/orm-mapping-db-to-ruby-object-nyc -web-060319
  5. Learn.co Обновление записей в ORM, Flatiron School, https://github.com/learn-co-students/orm-updating-records-nyc-web-060319
  6. Learn.co Предотвращение дублирования записей, Flatiron School, https://learn.co/tracks/web-development-immersive-2-0-module-one/orms-and-active- запись / orms / предотвращение-дублирования записи