En uno de mis artículos anteriores escribí sobre las tablas de almacenamiento de términos de Erlang (o simplemente ETS), que permiten que las tuplas de datos arbitrarios se almacenen en la memoria. También discutimos el ETS basado en disco (DETS), que proporciona una funcionalidad un poco más limitada, pero le permite guardar su contenido en un archivo.
A veces, sin embargo, es posible que necesite una solución aún más potente para almacenar los datos. Conozca Mnesia, un sistema de gestión de bases de datos distribuidas en tiempo real que se introdujo inicialmente en Erlang. Mnesia tiene un modelo de datos híbrido relacional / objeto y tiene muchas características interesantes, que incluyen replicación y búsquedas rápidas de datos..
En este artículo, aprenderás:
Vamos a empezar, vamos?
Entonces, como ya se mencionó anteriormente, Mnesia es un modelo de objeto y de datos relacionales que se escala realmente bien. Tiene un lenguaje de consulta DMBS y admite transacciones atómicas, como cualquier otra solución popular (Postgres o MySQL, por ejemplo). Las tablas de Mnesia pueden almacenarse en el disco y en la memoria, pero los programas pueden escribirse sin el conocimiento de la ubicación real de los datos. Además, puede replicar sus datos en múltiples nodos. También tenga en cuenta que Mnesia se ejecuta en la misma instancia de BEAM que todos los demás códigos.
Como Mnesia es un módulo de Erlang, deberías acceder a él usando un átomo:
mnesia
Aunque es posible crear un alias como este:
alias: mnesia, como: mnesia
Los datos en Mnesia se organizan en mesas que tienen sus propios nombres representados como átomos (que es muy similar a ETS). Las tablas pueden tener uno de los siguientes tipos:
:conjunto
-el tipo por defecto. No puede tener varias filas con exactamente la misma clave principal (veremos en un momento cómo definir una clave principal). Las filas no están siendo ordenadas de ninguna manera particular.: order_set
-igual que :conjunto
, pero los datos están ordenados por la clave primaria. Más adelante veremos que algunas operaciones de lectura se comportarán de manera diferente con : order_set
mesas.:bolso
-varias filas pueden tener la misma clave, pero las filas aún no pueden ser completamente idénticas.Las tablas tienen otras propiedades que se pueden encontrar en los documentos oficiales (veremos algunas de ellas en la siguiente sección). Sin embargo, antes de comenzar a crear tablas, necesitamos un esquema, por lo que vamos a la siguiente sección y agregamos una.
Para crear un nuevo esquema, usaremos un método con un nombre bastante sorprendente: create_schema / 1
. Básicamente, va a crear una nueva base de datos para nosotros en un disco. Acepta un nodo como argumento:
: mnesia.create_schema ([nodo ()])
Un nodo es una máquina virtual de Erlang que maneja sus comunicaciones, memoria y otras cosas. Los nodos pueden conectarse entre sí y no están limitados a una PC, también puede conectarse a otros nodos a través de Internet.
Después de ejecutar el código anterior, un nuevo directorio llamado Mnesia.nonode@nohost Se creará que va a contener su base de datos.. nonode @ nohost es el nombre del nodo aquí. Antes de que podamos crear cualquier tabla, sin embargo, se debe iniciar Mnesia. Esto es tan simple como llamar al inicio / 0
función:
: mnesia.start ()
Mnesia debe iniciarse en todos los nodos participantes, cada uno de los cuales normalmente tiene una carpeta en la que se escribirán los archivos (en nuestro caso, esta carpeta se llama Mnesia.nonode@nohost). Todos los nodos que componen el sistema Mnesia se escriben en el esquema, y luego puede agregar o eliminar nodos individuales. Además, al comenzar, los nodos intercambian información de esquema para asegurarse de que todo está bien.
Si Mnesia comenzó con éxito, un :De acuerdo
el átomo será devuelto como resultado. Más tarde puede detener el sistema llamando parada / 0
:
: mnesia.stop () # =>: detenido
Ahora podemos crear una nueva tabla. Como mínimo, deberíamos proporcionar su nombre y una lista de atributos para los registros (piense en ellos como columnas):
: mnesia.create_table (: usuario, [atributos: [: id,: nombre,: apellido]]) # => : atómico,: ok
Si el sistema no se está ejecutando, la tabla no se creará y : abortado, : node_not_running,: nonode @ nohost
error será devuelto en su lugar. Además, si la tabla ya existe, obtendrás un : abortado, : already_exists,: usuario
error.
Así se llama nuestra nueva tabla. :usuario
, y tiene tres atributos: :carné de identidad
, :nombre
, y :apellido
. Tenga en cuenta que el primer atributo de la lista siempre se utiliza como clave principal, y podemos utilizarlo para buscar rápidamente un registro. Más adelante en el artículo, veremos cómo escribir consultas complejas y agregar índices secundarios.
Además, recuerde que el tipo predeterminado para la tabla es :conjunto
, pero esto se puede cambiar muy fácilmente:
: mnesia.create_table (: usuario, [atributos: [: id,: nombre,: apellido], tipo:: bolsa])
Incluso puede hacer que su tabla sea de solo lectura configurando :modo de acceso
a :solo lectura:
: mnesia.create_table (: usuario, [atributos: [: id,: nombre,: apellido], escribe:: bag, access_mode: read_only])
Después de crear el esquema y la tabla, el directorio tendrá una esquema.DAT archivo así como algunos .Iniciar sesión archivos. Pasemos ahora a la siguiente sección e insertemos algunos datos en nuestra nueva tabla!
Para almacenar algunos datos en una tabla, necesita utilizar una función escribir / 1
. Por ejemplo, agreguemos un nuevo usuario llamado John Doe:
: mnesia.write (: usuario, 1, "John", "Doe")
Tenga en cuenta que hemos especificado tanto el nombre de la tabla como todos los atributos del usuario para almacenar. Intenta ejecutar el código ... y falla miserablemente con un : abortado,: no_transaction
error. ¿Por qué está pasando esto? Bueno, esto es porque el escribir / 1
La función debe ser ejecutada en una transacción. Si, por alguna razón, no desea mantener una transacción, la operación de escritura se puede realizar de forma "sucia" utilizando dirty_write / 1
:
: mnesia.dirty_write (: usuario, 1, "John", "Doe") # =>: ok
Este enfoque generalmente no se recomienda, por lo que en lugar de eso construyamos una transacción simple con la ayuda de transacción
función:
: mnesia.transaction (fn ->: mnesia.write (: usuario, 1, "John", "Doe") fin) # => : atomic,: ok
transacción
acepta una función anónima que tiene una o más operaciones agrupadas. Tenga en cuenta que en este caso el resultado es : atómico,: ok
, No solo :De acuerdo
como lo fue con el dirty_write
función. El principal beneficio aquí es que si algo sale mal durante la transacción, todas las operaciones se retrotraen..
En realidad, ese es un principio de atomicidad, que dice que o todas las operaciones deberían ocurrir o que ninguna operación debería ocurrir en caso de un error. Supongamos, por ejemplo, que está pagando a sus empleados sus salarios y, de repente, algo sale mal. La operación se detiene y, definitivamente, no desea terminar en una situación en la que algunos empleados obtuvieron sus salarios y otros no. Ahí es cuando las transacciones atómicas son realmente útiles..
los transacción
La función puede tener tantas operaciones de escritura como sea necesario:
write_data = fn ->: mnesia.write (: usuario, 2, "Kate", "Brown"): mnesia.write (: usuario, 3, "Will", "Smith") final: mnesia.transaction (write_data) # => : atomic,: ok
Curiosamente, los datos se pueden actualizar utilizando el escribir
funciona también Simplemente proporcione la misma clave y nuevos valores para los otros atributos:
update_data = fn ->: mnesia.write (: usuario, 2, "Kate", "Smith"): mnesia.write (: usuario, 3, "Will", "Brown") final: mnesia.transaction (actualizar datos)
Tenga en cuenta, sin embargo, que esto no va a funcionar para las tablas del :bolso
tipo. Debido a que tales tablas permiten que varios registros tengan la misma clave, simplemente obtendrá dos registros: [: usuario, 2, "Kate", "Brown", : usuario, 2, "Kate", "Smith"]
. Todavía, :bolso
Las tablas no permiten que existan registros totalmente idénticos..
De acuerdo, ahora que tenemos algunos datos en nuestra tabla, ¿por qué no intentamos leerlos? Al igual que con las operaciones de escritura, puede realizar la lectura de forma "sucia" o "transaccional". La "manera sucia" es más simple, por supuesto (¡pero ese es el lado oscuro de la Fuerza, Luke!):
: mnesia.dirty_read (: usuario, 2) # => [: usuario, 2, "Kate", "Smith"]
Asi que dirty_read
devuelve una lista de registros encontrados en función de la clave proporcionada. Si la mesa es una :conjunto
o un : order_set
, La lista tendrá un solo elemento. por :bolso
Tablas, la lista puede, por supuesto, tener múltiples elementos. Si no se encontraron registros, la lista estaría vacía.
Ahora intentemos realizar la misma operación pero utilizando el enfoque transaccional:
read_data = fn ->: mnesia.read (: user, 2) final: mnesia.transaction (read_data) => : atomic, [: user, 2, "Kate", "Brown"]
Genial!
¿Existen otras funciones útiles para leer datos? ¡Pero por supuesto! Por ejemplo, puede tomar el primer o el último registro de la tabla:
: mnesia.dirty_first (: usuario) # => 2: mnesia.dirty_last (: usuario) # => 2
Ambos sucio_primero
y sucia
tienen sus contrapartes transaccionales, a saber primero
y último
, Eso debería estar envuelto en una transacción. Todas estas funciones devuelven la clave del registro, pero tenga en cuenta que en ambos casos obtenemos 2
Como resultado, a pesar de que tenemos dos registros con las teclas 2
y 3
. Por qué está pasando esto?
Parece que para el :conjunto
y :bolso
mesas, las sucio_primero
y sucia
(tanto como primero
y último
) las funciones son sinónimos porque los datos no están ordenados en ningún orden específico. Si, sin embargo, tienes un : order_set
tabla, los registros se ordenarán por sus claves, y el resultado sería:
: mnesia.dirty_first (: usuario) # => 2: mnesia.dirty_last (: usuario) # => 3
También es posible agarrar la siguiente o la anterior tecla usando dirty_next
y dirty_prev
(o siguiente
y prev
):
: mnesia.dirty_next (: usuario, 2) => 3: mnesia.dirty_next (: usuario, 3) =>: "$ end_of_table"
Si no hay más registros, un átomo especial. : "$ end_of_table"
es regresado. Además, si la tabla es una :conjunto
o :bolso
, dirty_next
y dirty_prev
son sinónimos.
Por último, puede obtener todas las claves de una tabla usando dirty_all_keys / 1
o all_keys / 1
:
: mnesia.dirty_all_keys (: usuario) # => [3, 2]
Para borrar un registro de una tabla, use sucio_delete
o borrar
:
: mnesia.dirty_delete (: usuario, 2) # =>: ok
Esto va a eliminar todos los registros con una clave dada.
Del mismo modo, puede eliminar toda la tabla:
: mnesia.delete_table (: usuario)
No hay una contraparte "sucia" para este método. Obviamente, después de eliminar una tabla, no puede escribir nada en ella, y : abortado, : no_exists,: usuario
error será devuelto en su lugar.
Por último, si realmente estás en un estado de ánimo de eliminación, todo el esquema se puede eliminar usando delete_schema / 1
:
: mnesia.delete_schema ([nodo ()])
Esta operación devolverá un : error, 'Mnesia no se detiene en todas partes', [: nonode @ nohost]
error si Mnesia no se detiene, así que no olvides hacerlo:
: mnesia.stop (): mnesia.delete_schema ([nodo ()])
Ahora que hemos visto los aspectos básicos del trabajo con Mnesia, profundicemos un poco más y veamos cómo escribir consultas avanzadas. Primero, hay match_object
y dirty_match_object
funciones que se pueden usar para buscar un registro basado en uno de los atributos proporcionados:
: mnesia.dirty_match_object (: user,: _, "Kate", "Brown") # => [: user, 2, "Kate", "Brown"]
Los atributos que no le interesan están marcados con el : _
átomo. Puede establecer solo el apellido, por ejemplo:
: mnesia.dirty_match_object (: user,: _,: _, "Brown") # => [: user, 2, "Kate", "Brown"]
También puede proporcionar criterios de búsqueda personalizados utilizando seleccionar
y dirty_select
. Para ver esto en acción, primero llenemos la tabla con los siguientes valores:
write_data = fn ->: mnesia.write (: usuario, 2, "Kate", "Brown"): mnesia.write (: usuario, 3, "Will", "Smith"): mnesia.write ( : usuario, 4, "Will", "Smoth"): mnesia.write (: usuario, 5, "Will", "Smath") final: mnesia.transaction (write_data)
Ahora lo que quiero hacer es encontrar todos los registros que tienen Será
como el nombre y cuyas llaves son menos de 5
, lo que significa que la lista resultante debe contener solo "Will Smith" y "Will Smoth". Aquí está el código correspondiente:
: mnesia.dirty_select (: usuario, [: usuario,: "$ 1",: "$ 2",: "$ 3", [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Will", "Smith"], [4, "Will", "Smoth"]]
Las cosas son un poco más complejas aquí, así que analicemos este fragmento paso a paso.
: usuario,: "$ 1",: "$ 2",: "$ 3"
parte. Aquí proporcionamos el nombre de la tabla y una lista de parámetros posicionales. Deben estar escritos en esta forma de aspecto extraño para que podamos utilizarlos más adelante.. $ 1
corresponde a la :carné de identidad
, $ 2
es el nombre
, y $ 3
es el apellido
.:<, :"$1", 5
significa que nos gustaría seleccionar solo los registros cuyo atributo marcado como $ 1
(es decir, :carné de identidad
) es menos que 5
. : ==,: "$ 2", "Will"
, A su vez, significa que estamos seleccionando los registros con el :nombre
ajustado a "Será"
.[: "$$"]
significa que nos gustaría incluir todos los campos en el resultado. Podrías decir [: "$ 2"]
para mostrar sólo el nombre. Tenga en cuenta, por cierto, que el resultado contiene una lista de listas: [[3, "Will", "Smith"], [4, "Will", "Smoth"]]
.También puede marcar algunos atributos como los que no le interesa usar el : _
átomo. Por ejemplo, ignoremos el apellido:
: mnesia.dirty_select (: usuario, [: usuario,: "$ 1",: "$ 2",: _, [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Voluntad"], [4, "Voluntad"]]
En este caso, sin embargo, el apellido no se incluirá en el resultado..
Supongamos ahora que nos gustaría modificar nuestra tabla agregando un nuevo campo. Esto se puede hacer usando el mesa de transformación
función, que acepta el nombre de la tabla, una función que se aplica a todos los registros y la lista de nuevos atributos:
: mnesia.transform_table (: usuario, fn (: usuario, id, nombre, apellido) -> : usuario, id, nombre, apellido,: rand.uniform (1000) fin, [: id,: nombre, : apellido,: sueldo])
En este ejemplo estamos agregando un nuevo atributo llamado :salario
(Se proporciona en el último argumento). En cuanto a función de transformación (el segundo argumento), estamos configurando este nuevo atributo a un valor aleatorio. También puede modificar cualquier otro atributo dentro de esta función de transformación. Este proceso de cambio de datos se conoce como "migración", y este concepto debe ser familiar para los desarrolladores que vienen del mundo Rails..
Ahora puede simplemente obtener información sobre los atributos de la tabla usando table_info
:
: mnesia.table_info (: usuario,: atributos) # => [: id,: nombre,: apellido,: sueldo]
los :salario
atributo está ahí! Y, por supuesto, sus datos también están en su lugar:
: mnesia.dirty_read (: usuario, 2) # => [: usuario, 2, "Kate", "Brown", 778]
Puede encontrar un ejemplo un poco más complejo de usar ambos crear mesa
y mesa de transformación
Funciones en el sitio web de ElixirSchool..
Mnesia le permite hacer cualquier atributo indexado usando el add_table_index
función. Por ejemplo, hagamos nuestro :apellido
atributo indexado:
: mnesia.add_table_index (: usuario,: apellido) # => : atómico,: ok
Si el índice ya existe, obtendrá un error. : abortado, : already_exists,: usuario, 4
.
Como indica la documentación de esta función, los índices no vienen gratis. Específicamente, ocupan espacio adicional (proporcional al tamaño de la tabla) y hacen que las operaciones de inserción sean un poco más lentas. Por otro lado, le permiten buscar los datos más rápido, por lo que es una compensación justa.
Puede buscar por un campo indexado usando la tecla dirty_index_read
o index_read
función:
: mnesia.dirty_index_read (: usuario, "Smith",: apellido) # => [: usuario, 3, "Will", "Smith"]
Aquí estamos utilizando el índice secundario. :apellido
para buscar un usuario.
Puede ser algo tedioso trabajar directamente con el módulo Mnesia, pero afortunadamente hay un paquete de terceros llamado Amnesia (¡duh!) Que le permite realizar operaciones triviales con mayor facilidad.
Por ejemplo, puede definir su base de datos y una tabla como esta:
use Amnesia defdatabase Demo do Deftable User, [: id, autoincrement,: name,: surname, email], index: [: email] do end end end
Esto va a definir una base de datos llamada Manifestación
con una mesa Usuario
. El usuario va a nombrar un nombre, un apellido, un correo electrónico (un campo indexado) y una identificación (clave principal configurada para autoincremento).
A continuación, puede crear fácilmente el esquema utilizando la tarea de mezcla integrada:
mezcla amnesia.create -d Demo --disk
En este caso, la base de datos estará basada en el disco, pero puede configurar otras opciones disponibles. También hay una tarea de eliminación que, obviamente, destruirá la base de datos y todos los datos:
mezclar amnesia.drop -d demo
Es posible destruir tanto la base de datos como el esquema:
mix amnesia.drop -d Demo --schema
Con la base de datos y el esquema en su lugar, es posible realizar varias operaciones contra la tabla. Por ejemplo, crear un nuevo registro:
Amnesia.transaction do will_smith =% User nombre: "Will", apellido: "Smith", correo electrónico: "[email protected]" |> User.write end
O consigue un usuario por id:
Amnesia.transaction do will_smith = User.read (1) end
Además, puede definir un Mensaje
mesa mientras se establece una relación con el Usuario
mesa con un user_id
como clave externa:
Mensaje definible, [: id_usuario,: contenido] finaliza
Las tablas pueden tener un montón de funciones de ayuda dentro, por ejemplo, para crear un mensaje u obtener todos los mensajes:
Usuario definible, [: id, autoincrement,: name,: apellido,: email], índice: [: email] do def add_message (self, content) do% Message user_id: self.id, content: content | > Message.write end def messages (self) do Message.read (self.id) end end
Ahora puede encontrar al usuario, crear un mensaje para ellos o listar todos sus mensajes con facilidad:
Amnesia.transaction do will_smith = User.read (1) will_smith |> User.add_message "hi!" will_smith |> User.messages fin
Bastante simple, ¿no es así? Algunos otros ejemplos de uso se pueden encontrar en el sitio web oficial de Amnesia.
En este artículo, hablamos sobre el sistema de gestión de base de datos Mnesia disponible para Erlang y Elixir. Hemos discutido los conceptos principales de este DBMS y hemos visto cómo crear un esquema, una base de datos y tablas, así como realizar todas las operaciones principales: crear, leer, actualizar y destruir. Además, ha aprendido cómo trabajar con índices, cómo transformar tablas y cómo utilizar el paquete Amnesia para simplificar el trabajo con bases de datos..
Realmente espero que este artículo haya sido útil y que estén ansiosos por probar Mnesia en acción también. Como siempre, les agradezco que se queden conmigo y hasta la próxima.!